用GCD实现多任务

GCD(Grand Central Dispatch)是iOS多任务的核心。NSOperationQueue是在GCD的基础上实现的,基本原理都类似。这个是要你把NSOperation添加到NSOperationQueue,而GCD是要你把一个块添加到分派队列。

Note that 分派队列是队列,不是线程。队列不是接受块的东西,而是组织块的。所有的GCD方法都是把块添加到队列末尾,而不是让块运行。

块添加到分派队列后就无法取消了。分派队列是严格的FIFO结构,所以无法在队列中使用优先级或者调整块的次序。如果需要这一类的特性,直接用NSOperationQueue就好了,没有必要再用GCD去造轮子。

所有的分派队列自身都是线程安全的,你能从多个线程并行的访问它们。

队列目标和优先级

GCD中队列是有层级的。只有全局系统队列会被调度运行,可以用dispatch_get_global_queue和下面这些优先级常量中的一个来访问这些队列:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

这四个队列都是并行的。GCD会根据可用线程尽可能从高优先级队列调度块,等高优先级的队列空了以后,会继续调用默认优先级队列,以此类推。

除了这四个全局队列外,系统还提供一个叫主队列(main queue)的特殊队列,它是个串行队列。这个队列中的任务一次只能执行一个,但是它能够保证所有任务都在主线程上运行。你更新UI线程只能在主线程上更新,所以这个队列主要就是用于给UIView发送消息或者发送通知的。

我们自己创建一个队列时(我们暂时叫它A),它会默认附加到默认优先级队列上(暂时叫它B),当然你也可以手动来设置优先级。附加到的这个队列B就叫目标队列。当块到达队列A头部时,实际上会移动到目标队列B的尾部,当到达全局队列(也就是目标队列B)的头部时就会被执行。

刚才说过,分派队列是严格的执行FIFO结构的,也就是块被添加到队列后,会按照添加的顺序运行,无法取消,也不能改变相对于队列中其它块的相对顺序。那要想让高优先级块插队怎么办呢?代码如下:

// 创建两个队列,一个低优先级,一个高优先级
dispatch_queue_t low = dispatch_queue_create("low", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t high = dispatch_queue_create("high", DISPATCH_QUEUE_SERIAL);

// 使高优先级队列成为低优先级队列的目标队列
dispatch_set_target_queue(low, high);

//先分派的低优先级队列
dispatch_async(low, ^{ /* low_priority_block */});

//然后暂停低优先级队列,并且在高优先级块结束后恢复低优先级队列
dispatch_suspend(low);
dispatch_async(high, ^{
	/* high_priority_block */
	dispatch_resume(low);
});

暂停队列会阻止调度开始就处于该队列的任何块,还有任何以该队列为目标队列的队列, 当然了!!头都没被调用,尾巴急个蛋蛋。但是暂停不会停止正在执行的快,当然了,不可能裤子都脱了,然后拍拍屁股走人吧。

系统提供的dispatch方法

为了方便使用GCD,苹果提供了一些方法方便我们将block放在主线程或者后台线程执行,或者延后执行。

// 后台执行
dispatch_async(dispatch_get_global_queue(0,0), ^{
	// something
});

// 主线程执行
dispatch_async(dispatch_get_main_queue(0,0), ^{
	// something
}

// 一次性执行
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	// code to be executed once
});

// 延迟2秒执行
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
	// code to be executed on the main queue after delay
});

分派组

分派组类似于NSOperation中的依赖关系。首先创建一个组:

dispatch_group_t group = dispatch_group_create();

然后通过dispatch_group_async把块添加到组,类似于dispatch_async:

dispatch_group_async(group, queue, block);

然后用dispatch_group_notify(group, queue, block)注册一个块,当组执行完成后调用它:

dispatch_group_notify(group, queue, block);

当组里所有的块都执行完毕后,block就会被调度到queue上。如果调用dispatch_group_notify时队列上没有任何块,那么会马上出发通知。可以在配置组时调用dispatch_suspend暂停队列来防止这种情况,配置完成后调用dispatch_resume启动队列。

下面是一个例子:在网上下载两张不同的图片,显示到不同的UIImageView上去。

- (UIImage *)imageWithURLString:(NSString *)urlString {
    NSURL *url = [NSURL URLWithString:urlString];
    NSData *data = [NSData dataWithContentsOfURL:url];
    return [[UIImage alloc] initWithData:data];
}

- (void)downloadImages {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 异步下载图片
    dispatch_async(queue, ^{
        // 创建一个组
        dispatch_group_t group = dispatch_group_create();
        
        __block UIImage *image1 = nil;
        __block UIImage *image2 = nil;
        
        // 关联一个任务到group
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 下载第一张图片
            NSString *url1 = @"yourURL";
            image1 = [self imageWithURLString:url1];
        });
        
        // 关联一个任务到group
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 下载第一张图片
            NSString *url2 = @"yourURL";
            image2 = [self imageWithURLString:url2];
        });
        
        // 等待组中的任务执行完毕,回到主线程执行block回调
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            self.imageView1.image = image1;
            self.imageView2.image = image2;
        });
    });
}

再强调一遍更新UIView一定要在主线程上

小结

乱乱糟糟写了这么多,有些知识是从网上学到的,有些是书上的。有些描述可能不太详尽,也可能不够准确,有异议的地方请发邮件指出,谢谢。