浅谈GCD

GCD的简介

  • 1.什么是GCD
    • 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”
    • 纯C语言,提供了非常多强大的函数
  • 2.GCD的优势
    • GCD是苹果公司为多核的并行运算提出的解决方案
    • GCD会自动利用更多的CPU内核(比如双核、四核)
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
  • 3.GCD中有2个核心概念

    • 任务:执行什么操作
    • 队列:用来存放任务
  • 4.GCD的使用就2个步骤

    • 定制任务
      • 确定想做的事情
    • 将任务添加到队列中
      • GCD会自动将队列中的任务取出,放到对应的线程中执行
      • 任务的取出遵循队列的FIFO原则:先进先出,后进后出
  • 5.有4个术语比较容易混淆:同步、异步、并发、串行
    • 同步和异步主要影响:能不能开启新的线程
      • 同步:只是在当前线程中执行任务,不具备开启新线程的能力
      • 异步:可以在新的线程中执行任务,具备开启新线程的能力
    • 并发和串行主要影响:任务的执行方式
      • 并发:允许多个任务并发(同时)执行
      • 串行:一个任务执行完毕后,再执行下一个任务

GCD的各种组合

  • 1.异步 + 并发
/*
 异步 + 并发 : 会开启新的线程
 如果任务比较多, 那么就会开启多个线程
 */
- (void)asynConcurrent
{
    /*
     执行任务
     dispatch_async
     dispatch_sync
     */

    /*
     第一个参数: 队列的名称
     第二个参数: 告诉系统需要创建一个并发队列还是串行队列
     DISPATCH_QUEUE_SERIAL :串行
     DISPATCH_QUEUE_CONCURRENT 并发
     */
    //    dispatch_queue_t queue = dispatch_queue_create("com.longshao.lsl", DISPATCH_QUEUE_CONCURRENT);

    // 系统内部已经给我们提供好了一个现成的并发队列
    /*
     第一个参数: iOS8以前是优先级, iOS8以后是服务质量
     iOS8以前
     *  - DISPATCH_QUEUE_PRIORITY_HIGH          高优先级 2
     *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      默认的优先级 0
     *  - DISPATCH_QUEUE_PRIORITY_LOW:          低优先级 -2
     *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:

     iOS8以后
     *  - QOS_CLASS_USER_INTERACTIVE  0x21 用户交互(用户迫切想执行任务)
     *  - QOS_CLASS_USER_INITIATED    0x19 用户需要
     *  - QOS_CLASS_DEFAULT           0x15 默认
     *  - QOS_CLASS_UTILITY           0x11 工具(低优先级, 苹果推荐将耗时操作放到这种类型的队列中)
     *  - QOS_CLASS_BACKGROUND        0x09 后台
     *  - QOS_CLASS_UNSPECIFIED       0x00 没有设置

     第二个参数: 废物
     */
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    /*
     第一个参数: 用于存放任务的队列
     第二个参数: 任务(block)

     GCD从队列中取出任务, 遵循FIFO原则 , 先进先出
     输出的结果和苹果所说的原则不符合的原因: CPU可能会先调度其它的线程

     能够创建新线程的原因:
     我们是使用"异步"函数调用
     能够创建多个子线程的原因:
     我们的队列是并发队列
     */
    dispatch_async(queue, ^{
        NSLog(@"任务1  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务2  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务3  == %@", [NSThread currentThread]);
    });
}
  • 2.异步 + 串行
/*
 异步 + 串行:会开启新的线程
但是只会开启一个新的线程
 注意: 如果调用 异步函数, 那么不用等到函数中的任务执行完毕, 就会执行后面的代码
 */
- (void)asynSerial
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.longshao.lsl", DISPATCH_QUEUE_SERIAL);
    /*
     能够创建新线程的原因:
     我们是使用"异步"函数调用
     只创建1个子线程的原因:
     我们的队列是串行队列
     */
    // 2.将任务添加到队列中
    dispatch_async(queue, ^{
        NSLog(@"任务1  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务2  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务3  == %@", [NSThread currentThread]);
    });

    NSLog(@"--------");
}
  • 3.同步 + 串行
/*
 同步 + 串行: 不会开启新的线程
 注意点: 如果是调用 同步函数, 那么会等同步函数中的任务执行完毕, 才会执行后面的代码
 */
- (void)syncSerial
{
    // 1.创建一个串行队列
    // #define DISPATCH_QUEUE_SERIAL NULL
    // 所以可以直接传NULL
    dispatch_queue_t queue = dispatch_queue_create("com.longshao.lsl", NULL);

    // 2.将任务添加到队列中
    dispatch_sync(queue, ^{
        NSLog(@"任务1  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任务2  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任务3  == %@", [NSThread currentThread]);
    });

    NSLog(@"---------");
}
  • 4.同步 + 并发
/*
 同步 + 并发 : 不会开启新的线程
 妻管严
 */
- (void)syncConCurrent
{
    // 1.创建一个并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 2.将任务添加到队列中
    dispatch_sync(queue, ^{
        NSLog(@"任务1  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任务2  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任务3  == %@", [NSThread currentThread]);
    });

     NSLog(@"---------");
}
  • 5.异步 + 主队列
/*
 异步 + 主队列 : 不会创建新的线程, 并且任务是在主线程中执行
 */
- (void)asyncMain
{
    // 主队列:
    // 特点: 只要将任务添加到主队列中, 那么任务"一定"会在主线程中执行 \
    无论你是调用同步函数还是异步函数
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
}
  • 6.在主线程中调用同步函数 + 主队列
/*
 如果是在主线程中调用同步函数 + 主队列, 那么会导致死锁
 导致死锁的原因:
 sync函数是在主线程中执行的, 并且会等待block执行完毕. 先调用
 block是添加到主队列的, 也需要在主线程中执行. 后调用
 */
- (void)syncMain
{
    NSLog(@"%@", [NSThread currentThread]);
    // 主队列:
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  如果是调用 同步函数, 那么会等同步函数中的任务执行完毕, 才会执行后面的代码
    // 注意: 如果dispatch_sync方法是在主线程中调用的, 并且传入的队列是主队列, 那么会导致死锁
    dispatch_sync(queue, ^{
        NSLog(@"----------");
        NSLog(@"%@", [NSThread currentThread]);
    });
    NSLog(@"----------");
}
  • 7.在子线程中调用 同步函数 + 主队列
/*
 如果是在子线程中调用 同步函数 + 主队列, 那么没有任何问题
 */
- (void)syncMain2
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // block会在子线程中执行
        //        NSLog(@"%@", [NSThread currentThread]);

        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            // block一定会在主线程执行
            NSLog(@"%@", [NSThread currentThread]);
        });
    });
    NSLog(@"------------");
}

GCD线程间通信

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"--------");
    // 1.除主队列以外, 随便搞一个队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 2.调用异步函数
    dispatch_async(queue, ^{
        // 1.下载图片
        NSURL *url = [NSURL URLWithString:@"http://pic.4j4j.cn/upload/pic/20130531/07ed5ea485.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 2.将二进制转换为图片
        UIImage *image = [UIImage imageWithData:data];

        // 3.回到主线程更新UI
//        self.imageView.image = image;
        /*
         技巧:
         如果想等UI更新完毕再执行后面的代码, 那么使用同步函数
         如果不想等UI更新完毕就需要执行后面的代码, 那么使用异步函数
         */
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
        NSLog(@"设置图片完毕 %@", image);
    });
}

GCD的延迟执行

  • 1.使用GCD延迟执行的好处:

    • 以前我们需要延迟执行某个任务的时候,可以使用NSTimer或performSelector:withObject:afterDelay:方法来实现。但是使用NSTimer的时候会出现一些问题,比如用NSTimer做定时图片轮播时,当拖动图片会影响图片的正常轮播效果,需要通过监听拖拽事件来设置NSTimer的定时时间,这样使用会很麻烦,且不利于后期的维护。使用GCD的延迟执行没有上面的问题,而且代码都集中在一块利于维护和阅读。
  • 2.示例如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //    [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(demo) userInfo:nil repeats:NO];

    //    [self performSelector:@selector(demo) withObject:nil afterDelay:3.0];

    // 该方法中, 会根据传入的队列来决定回掉block在哪个线程中执行
    // 如果传入的是主队列, 那么block会在主线程调用
    // 如果传入的是全局队列, 那么block会在子线程中调用
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"3秒之后执行  %@", [NSThread currentThread]);
    });

}

- (void)demo
{
    NSLog(@"%s", __func__);
}

GCD的一次代码(和懒加载的区别)

  • 1.Person类
#import "Person.h"

@implementation Person
- (NSArray *)books
{
    /*
    if (!_books) {
        _books = @[
                   @"iOS开发",
                   @"Android开发",
                   @"高薪技巧"
                   ];
    }
     */

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _books = @[
                   @"iOS开发",
                   @"Android开发",
                   @"高薪技巧"
                   ];
    });
    return _books;
}
@end
  • 2.调用类
#import "Person.h"

@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    /*
     // 注意: 千万不能把一次性代码当作懒加载来使用
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
     NSLog(@"我被执行了");
     });
     */
    Person *p1 = [Person new];
    NSLog(@"%@", p1.books);

    Person *p2 = [Person new];
    NSLog(@"%@", p2.books);
}
@end
  • 3执行结果:

    • (1)懒加载时:
      --- (
      "iOS\U5f00\U53d1",
      "Android\U5f00\U53d1",
      "\U9ad8\U85aa\U6280\U5de7"
      )
      --- (
      "iOS\U5f00\U53d1",
      "Android\U5f00\U53d1",
      "\U9ad8\U85aa\U6280\U5de7"
      )
      
  • (2)使用GCD一次性代码时:

    --- (
      "iOS\U5f00\U53d1",
      "Android\U5f00\U53d1",
      "\U9ad8\U85aa\U6280\U5de7"
    )
    --- (null)
    

GCD的遍历方法

  • 1.GCD的遍历方法dispatch_apply,可以开启多个线程同时执行某个任务来提高效率。
  • 2.比如在多文件转移的时候,用for循环和GCD的遍历方法dispatch_apply的对比:
    • 首先时用for循环转移文件:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 1.定义变量记录原始文件夹和目标文件夹的路径
    NSString *sourcePath = @"/Users/lisilong/Desktop/test";
    NSString *destPath = @"/Users/lisilong/Desktop/longshao";

    // 2.取出原始文件夹中所有的文件
    NSFileManager *manager = [NSFileManager defaultManager];
    NSArray *files = [manager subpathsAtPath:sourcePath];

    // 开始转移文件时的时间
    NSDate *beginDate = [NSDate date];

    // 3.开始拷贝文件
     for (NSString *fileName in files) {
        // 3.1生产原始文件的绝对路径
        NSString *sourceFilePath = [sourcePath stringByAppendingPathComponent:fileName];

         // 3.2生产目标文件的绝对路径
        NSString *destFilePath = [destPath stringByAppendingPathComponent:fileName];

        // 3.3利用NSFileManager拷贝文件
        [manager moveItemAtPath:sourceFilePath toPath:destFilePath error:nil];
     }

    // 文件转移结束时的时间
    NSDate *endDate = [NSDate date];

    // 转移文件用时:
    NSLog(@"time=%f",[endDate timeIntervalSinceDate:beginDate]);
}
  • 用for循环转移文件用时:
--- time=1.139403
  • 用GCD的dispatch_apply遍历方法的转移文件:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 1.定义变量记录原始文件夹和目标文件夹的路径
    NSString *sourcePath = @"/Users/lisilong/Desktop/test";
    NSString *destPath = @"/Users/lisilong/Desktop/longshao";

    // 2.取出原始文件夹中所有的文件
    NSFileManager *manager = [NSFileManager defaultManager];
    NSArray *files = [manager subpathsAtPath:sourcePath];

    // 开始转移文件时的时间
    NSDate *beginDate = [NSDate date];

    // 3.开始拷贝文件
    /*
     第一个参数: 需要遍历几次
     第二个参数: 决定第三个参数的block在哪个线程中执行
     第三个参数: 回掉
     */
     dispatch_apply(files.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSString *fileName = files[index];
        // 3.1生产原始文件的绝对路径
        NSString *sourceFilePath = [sourcePath stringByAppendingPathComponent:fileName];

        // 3.2生产目标文件的绝对路径
        NSString *destFilePath = [destPath stringByAppendingPathComponent:fileName];

        // 3.3利用NSFileManager拷贝文件
        [manager moveItemAtPath:sourceFilePath toPath:destFilePath error:nil];
    });

    // 文件转移结束时的时间
    NSDate *endDate = [NSDate date];

    // 转移文件用时:
    NSLog(@"time=%f",[endDate timeIntervalSinceDate:beginDate]);
}
  • 用GCD的dispatch_apply遍历方法的转移文件用时
--- time=0.626504
  • 结论:用GCD的dispatch_apply遍历方法的效率比for循环的相对来说要高。

GCD控制任务之间的关系

  • 1.使用场景
    • 当某个任务需要在其它任务执行完后才能执行时,可以用GCD的栅栏(barrier)和组(group)来实现。
  • 2.GCD栅栏(barrier)的简介
    • 功能:
      • 拦截前面的任务, 只有先添加到队列中的任务=执行完毕, 才会执行栅栏添加的任务
      • 如果栅栏后面还有其它的任务, 那么必须等栅栏执行完毕才会执行后面的其它任务
    • 注意点:
      • 如果想要使用栅栏, 那么就不能使用全局的并发队列
      • 如果想使用栅栏, 那么所有的任务都必须添加到同一个队列中
  • 3.GCD组(group)的简介
    • 将队列放到group中, 当队列中的任务执行完毕时, group就会发出一个通知,然后执行相应的操作。
  • 4.GCD的栅栏和组的对比:
    • GCD的栅栏和组都可以实现,控制其它任务执行完后再执行某个任务。
    • 相对于组来说,栅栏还可以限制栅栏代码后面的代码的执行,即先执行完栅栏内的代码再执行其后的代码。
  • 5.示例,用多图片下载,来介绍GCD的栅栏和组:
  • (1)栅栏(barrier)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_queue_create("com.longshao.lsl", DISPATCH_QUEUE_CONCURRENT);

    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;
    // 1.开启一个新的线程下载第一张图片
    dispatch_async(queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/77c6a7efce1b9d1632701663f5deb48f8c546479.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image1 = image;
        NSLog(@"图片1下载完毕");
    });

    // 2.开启一个新的线程下载第二张图片
    dispatch_async(queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/18d8bc3eb13533fa0f2eb8c0acd3fd1f40345b47.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image2 = image;
        NSLog(@"图片2下载完毕");
    });

    // 3.开启一个新的线程, 合成图片
    // 栅栏
    dispatch_barrier_async(queue, ^{
        // 1.开启图片上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        // 2.将第一张图片画上去
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];
        // 3.将第二张图片画上去
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];
        // 4.从上下文中获取绘制好的图片
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
        // 5.关闭上下文
        UIGraphicsEndImageContext();

        // 4.回到主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });

        NSLog(@"栅栏执行完毕了");
    });

    // 只要栅栏执行完毕后才会执行
    dispatch_async(queue, ^{
        NSLog(@"---------");
    });
}
  • (2)组(group)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_queue_create("com.longshao.lsl", DISPATCH_QUEUE_CONCURRENT);

    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;

    dispatch_group_t group = dispatch_group_create();

    // 1.开启一个新的线程下载第一张图片
    dispatch_group_async(group, queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/77c6a7efce1b9d1632701663f5deb48f8c546479.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image1 = image;
        NSLog(@"图片1下载完毕");
    });

    // 2.开启一个新的线程下载第二张图片
    dispatch_group_async(group, queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/18d8bc3eb13533fa0f2eb8c0acd3fd1f40345b47.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image2 = image;
        NSLog(@"图片2下载完毕");
    });

    // 3.开启一个新的线程, 合成图片
    // 只要将队列放到group中, 队列中的任务执行完毕, group就会发出一个通知
    dispatch_group_notify(group, queue, ^{
        NSLog(@"%@ %@", image1, image2);
        // 1.开启图片上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        // 2.将第一张图片画上去
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];
        // 3.将第二张图片画上去
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];
        // 4.从上下文中获取绘制好的图片
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
        // 5.关闭上下文
        UIGraphicsEndImageContext();

        // 4.回到主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    });
}

你可能感兴趣的:(iOS那些事)