系列文章目录

GCD学习笔记<一>
GCD学习笔记<二>
GCD学习笔记<三>

初识GCD

Grand Central Dispatch(GCD)是iOS平台最强大的多线程技术之一,它用非常简单的记述方法,实现了复杂而又难懂的多线程编程。最通常的情况下,我们使用它做异步请求,由于block的大量使用,异步请求代码会和逻辑代码放在一起,这样代码结构看起来会更清晰。
本系列文章主要参考《Objective-C 高级编程 iOS与OS X多线程和内存管理》,强烈推荐想要进阶的童鞋啃这本书,虽然有部分东西过时,但绝对是进阶必备之好书。

dispatch_async()

dispatch_async()是最常用的方法之一,用法是这样:

dispatch_async(queue, ^{
        // 进行网络请求、写入文件等需要大量时间的操作
        dispatch_async(dispatch_get_main_queue(), ^{
            //更新UI
        });
    });

dispatch_async第一个参数是dispatch_queue_t类型的队列,暂且不管它,第二个参数即为一个block。dispatch_get_main_queue()是获取应用主线程,iOS更新UI都要在主线程进行。

dispatch_queue_t

dispatch_queue_t从名字看就能看出来,这是一个Dispatch Queue(调度队列)。在iOS中Dispatch Queue分为两种

  • Serial Dispatch Queue (串行调度队列)
  • Concurrent Dispatch Queue (并行调度队列)

串行,即只能按First-In-First-Out(FIFO)的顺序执行,也就是说,必须等待前面的事务处理完才能够被执行,而并行则不需要,这样的话,并行执行的操作的顺序不能得到保障。这里要注意,在程序执行的过程中,多个Serial Dispatch Queue间是并发执行的。

系统针对这两种类型各提供一个帮我们已经生成的队列 Main Dispatch Queue 和 Global Dispatch Queue。前者是一种Serial Dispatch Queue,而后者则是一种Concurrent Dispatch Queue。
Main Dispatch Queue中的处理是在主线程中进行的,通常情况下我们在这里进行更新UI,这个NSObject类的performSelectorOnMainThread:方法相对应。我们使用dispatch_get_main_queue()方法来获取Main Dispatch Queue。
Global Dispatch Queue则是一个并发队列,我们在这个队列里放一些网络请求等消耗时间较长的操作,与NSObject的performSelectorInBackground:相对应。Global Dispatch Queue中的执行操作有四种优先级,由高到低分别为High Priority, Default Priority, Low Priority, Background Priority, 优先级体现在两个方面,一方面是使用CPU的优先级,当然需要说明的时,这并不能保证程序的实时性,只可作为大概的估计,另一方面是用于多个queue之间资源的竞争,优先级高的Queue更优先拿到资源,当然,其实CPU也可以看做一种资源。High Priority的Queue会首先得到CPU,而Low Priority的Queue在没有资源竞争时可以拿到资源,这个Queue的优先级已经相当低了,甚至在UI活动的时候就不会得到运行。
我们使用dispatch_get_global_queue()获取Global Dispatch Queue

    // 高优先级
    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);


    // 默认优先级
    dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 低优先级
    dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);


    // background优先级
    dispatch_queue_t queue4 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_queue_create && dispatch_set_target_queue

dispatch_queue_t可以通过dispatch_queue_create方法来产生

dispatch_queue_t queue = dispatch_queue_create("xyz.ypli.gcd.SerialDispatchQueue", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue2 = dispatch_queue_create("xyz.ypli.gcd.SerialDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create方法的第一个参数是队列的名称,名称格式推荐完全限定域名(Fully Qualified Domain Name, FQDN), 主要作为调试提醒使用,所以一定不要嫌麻烦而填写个NULL,到头来麻烦的是自己,值得注意的是第一个参数是const char * 类型而非NSString类型。 第二个参数决定dispatch_queue_t的类型,像例子这样如果传入DISPATCH_QUEUE_SERIAL(其实这个值为NULL,所以传入NULL有同样效果),则产生的是Serial Dispatch Queue。想产生Concurrent Dispatch Queue 则需要传入DISPATCH_QUEUE_CONCURRENT。现在dispatch_queue_t类型已经可以像正常的类对象一样使用ARC进行管理了,如果是MRC的工程的话,是需要借助dispatch_retain(queue)dispatch_release(queue)来进行手动管理的。
dispatch_queue_create创建的队列优先级都可以是默认优先级(即,DISPATCH_QUEUE_PRIORITY_DEFAULT),Apple并没有给我们直接创建别的优先级队列的API,当我们想要改变队列的优先级的时候就需要用到dispatch_set_target_queue,此函数的第一个参数为原队列,第二个参数为目标队列。
就像这个傻傻的样子

dispatch_queue_t queue = dispatch_queue_create("xyz.ypli.gcd.SerialDispatchQueue", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));

dispatch_set_target_queue的目标队列是Concurrent Dispatch Queue时,用来变更Dispath Queue的执行优先级。当其目标队列均为Serial Dispatch Queue时,可以控制Serial Dispatch Queue之间的执行序列,而非并发执行,如下图。
(注意,此方法并不在乎原队列是什么类型的队列,执行效果只与目标队列有关)

dispatch_after

我们时常会在几秒后执行某个操作,这时就用到了dispatch_after

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"当你看到这条消息已经是三秒后了:)");
    });

dispatch_after有三个参数,第一个参数是dispatch_time_t类型,第二个参数即是操作所在的线程,第三个就是block了。
首先我们来看看dispatch_time_t类型,我们使用DISPATCH_TIME_NOW其实是获取一个dispatch_time_t,而dispatch_time方法其实是获取距离第一个dispatch_time_t之后一段时间的dispatch_time_t。第二个参数是一个int64_t类型,其就是long long类型,我们所写的ull其实是unsigned long long 的意思,NSEC_PER_SEC是以秒为单位,当然还有NSEC_PER_MSEC是以毫秒为单位。
当然,我们偶尔也会用到NSDate转换为dispatch_time_t,这里只贴出来代码,有兴趣的可以研究一下

dispatch_time_t getDispatchTimeByDate(NSDate *date) {
    NSTimeInterval interval = [date timeIntervalSince1970];
    double second;
    double subsecond = modf(interval, &second);
    struct timespec time;
    time.tv_sec = second;
    time.tv_nsec  = subsecond * NSEC_PER_SEC;
    return dispatch_walltime(&time, 0);
}

注意需要说明的是,这里的时间并非准确的时间,事实上,如果要求精度高的话,还需要考虑NSRunLoop循环周期,所以不建议使用此接口进行对时间精度要求很高的操作。