1.3 使用GCD实现多线程
前面介绍了使用NSThread实现多线程编程的方式,不难发现,这种方式实现多线程比较复杂,需要开发者自己控制多线程的同步、多线程的并发,稍不留神,往往就会出现错误,这对于一般的开发者来说是比较困难的。为了简化多线程应用的开发,iOS提供了GCD实现多线程。接下来,本节将针对GCD的有关内容进行详细的讲解。
1.3.1 GCD简介
在众多实现多线程的方案中,GCD应该是“最有魅力”的,这是因为GCD本身就是苹果公司为多核的并行运算提出的解决方案,工作时会自动利用更多的处理器核心。如果要使用GCD,系统会完全管理线程,开发者无需编写线程代码。
GCD是Grand Central Dispatch的缩写,它是基于C语言的。GCD会负责创建线程和调度需要执行的任务,由系统直接提供线程管理,换句话说就是GCD用非常简洁的方法,实现了极为复杂烦琐的多线程编程,这是一项划时代的技术。GCD有两个核心的概念,分别为队列和任务,针对这两个概念的介绍如下。
1. 队列
Dispatch Queue(队列)是GCD的一个重要的概念,它就是一个用来存放任务的集合,负责管理开发者提交的任务。队列的核心理念就是将长期运行的任务拆分成多个工作单元,并将这些单元添加到队列中,系统会代为管理这些队列,并放到多个线程上执行,无需开发者直接启动和管理后台线程。
系统提供了许多预定义的队列,包括可以保证始终在主线程上执行工作的Dispatch Queue,也可以创建自定义的Dispatch Queue,而且可以创建任意多个。队列会维护和使用一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。GCD的Dispatch Queue严格遵循FIFO(先进先出)原则,添加到Dispatch Queue的工作单元将始终按照加入Dispatch Queue的顺序启动,如图1-22所示。
图1-22 任务的先进先出原则
从图1-22中可以看出,task1是最先进入队列的,处理完毕后,最先从队列中移除,其余的任务则按照进入队列的顺序依次处理。需要注意的是,由于每个任务的执行时间各不相同,先处理的任务不一定先结束。
根据任务执行方式的不同,队列主要分为两种,分别如下。
(1)Serial Dispatch Queue(串行队列)。
串行队列底层的线程池只有一个线程,一次只能执行一个任务,前一个任务执行完成之后,才能够执行下一个任务,示意图如图1-23所示。
图1-23 串行队列
由图1-23可知,串行队列只能有一个线程,一旦task1添加到该队列后,task1就会首先执行,其余的任务等待,直到task1运行结束后,其余的任务才能依次进入处理。
(2)Concurrent Dispatch Queue(并发队列)。
并行队列底层的线程池提供了多个线程,可以按照FIFO的顺序并发启动、执行多个任务,示意图如图1-24所示。
图1-24 并发队列
由图1-24可知,并发队列中有4个线程,4个任务分别分配到任意一个线程后并发执行,这样可以使应用程序的响应性能显著提高。
2. 任务
任务就是用户提交给队列的工作单元,也就是代码块,这些任务会交给维护队列的线程池执行,因此这些任务会以多线程的方式执行。
综上所述,如果开发者要想使用GCD实现多线程,仅仅需要两个步骤即可,具体如下:
(1)创建队列;
(2)将任务的代码块提交给队列。
1.3.2 创建队列
要想创建队列,需要获取一个dispatch_queue_t类型的对象。为此,iOS提供了多个创建或者访问队列的函数,大体归纳为3种情况,具体介绍如下。
1. 获取全局并发队列(Global Concurrent Dispatch Queue)
全局并发队列可以同时并行地执行多个任务,但并发队列仍然按先进先出的顺序来启动任务。并发队列会在前一个任务完成之前就启动下一个任务并开始执行,它同时执行的任务数量会根据应用和系统动态变化,主要影响因素包括可用核数量、其他进程正在执行的工作数量、其他串行队列中优先任务的数量等。
系统给每个应用提供多个并发的队列,整个应用内全局共享。开发者不需要显式地创建这些队列,只需要使用dispatch_get_global_queue()函数来获取这些队列,函数定义如下:
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
在上述代码中,该函数有两个参数,第2个参数是供以后使用的,传入0即可。第1个参数用于指定队列的优先级,包含4个宏定义的常量,定义格式如下:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高 #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中) #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低 #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
由上至下,这些值表示的优先级依次降低,分别表示高、中、低、后台,默认为中。以DISPATCH_QUEUE_PRIORITY_DEFAULT举例,获取系统默认的全局并发队列可以通过如下代码完成:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DE FAULT, 0);
2. 创建串行和并行队列(Serial And Concurrent Dispatch Queue)
应用程序的任务如果要按照特定的顺序执行,需要使用串行队列,并且每次只能执行一项任务。尽管应用能够创建任意数量的队列,但不要为了同时执行更多的任务而创建更多的队列。如果需要并发地执行大量的任务,应该把任务提交到全局并发队列。
开发者必须显式地创建和管理所有使用的串行队列,使用dispatch_queue_create()函数根据指定的字符串创建串行队列,函数定义如下所示:
dispatch_queue_t dispatch_queue_create(const char *label,dispatch_queue_attr_t attr);
在上述代码中,该函数有两个参数,第1个参数是用来表示队列的字符串,可以选择设置,也可以为NULL。第2个参数用于控制创建的是串行队列还是并发队列,若将参数设置为“DISPATCH_QUEUE_SERIAL”,则表示串行队列;若将参数设置为“DISPATCH_QUEUE_CONCURRENT”,则表示并发队列;若设置为NULL,则默认为串行队列。例如,label参数的值为“itcast.queue”,attr参数的值为NULL,创建串行队列可以通过如下代码完成:
dispatch_queue_t queue = dispatch_queue_create("itcast.queue", NULL);
要注意的是,实际应用中,如果要使用并发队列,一般获取全局并发队列即可。
3. 获取主队列(Main Queue)
主队列是GCD自带的一个特殊的串行队列,只要是提交给主队列的任务,就会放到主线程中执行。使用dispatch_get_main_queue()函数可以获取主队列,函数定义如下:
dispatch_queue_t dispatch_get_main_queue(void);
在上述代码中,该函数只有一个返回值,而没有参数,获取主线程关联的队列可以通过如下代码完成:
dispatch_queue_t queue = dispatch_get_main_queue();
1.3.3 提交任务
队列创建完成之后,需要将任务代码块提交给队列。若要向队列提交任务,可通过同步和异步两种方式实现,具体介绍如下。
1. 以同步的方式执行任务
所谓同步执行任务,就是只会在当前线程中执行任务,不具备开启新线程的能力。少数情况下,开发者可能希望同步地调用任务,避免竞争条件或者其他同步错误。通过dispatch_sync()和dispatch_sync_f()函数能够同步地添加任务到队列,这两个函数会阻塞当前调用线程,直到相应的任务完成执行,这两个函数的定义格式如下:
void dispatch_sync(dispatch_queue_t queue,^(void)block); void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
在上述定义格式中,这两个函数都没有返回值,而且第2个函数多一个参数,针对它们的介绍如下。
- dispatch_sync()函数:将代码块以同步的方式提交给指定队列,该队列底层的线程池将负责执行该代码块。其中,第1个参数表示任务将添加到的目标队列,第2个参数就是将要执行的代码块,也就是要执行的任务。
- dispatch_sync_f()函数:将函数以同步的方式提交给指定队列,该队列底层的线程池将负责执行该函数。其中,第1个参数与前面相同,第2个参数是向函数传入应用程序定义的上下文,第3个参数是要传入的其他需要执行的函数。
为了大家更好地掌握,接下来,通过一个简单的案例,讲述如何使用同步的方式向串行队列和并发队列提交任务,具体步骤如下。
(1)新建一个Single View Application应用,名称为04-Dispatch Syn。
(2)进入Main.StoryBoard,从对象库拖曳两个Button到程序界面,用于控制串行或并行地执行同步任务,设置两个Button的Title分别为“串行同步任务”和“并行同步任务”。
(3)采用拖曳的方式,为“串行同步任务”按钮和“并行同步任务”按钮添加两个单击响应事件,分别命名为synSerial:和synConcurrent:。进入ViewController.m,实现这两个响应按钮单击的方法,如例1-4所示。
【例1-4】ViewController.m
1 #import "ViewController.h" 2 @interface ViewController () 3 - (IBAction)synSerial:(id)sender; 4 - (IBAction)synConcurrent:(id)sender; 5 @end 6 @implementation ViewController 7 dispatch_queue_t serialQueue; 8 dispatch_queue_t globalQueue ; 9 - (void)viewDidLoad { 10 [super viewDidLoad]; 11 // 创建串行队列 12 serialQueue = dispatch_queue_create("cn.itcast", DISPATCH_QUEUE_SERIAL); 13 // 获取全局并发队列 14 globalQueue = 15 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 16 } 17 // 单击“串行同步任务”后执行的行为 18 - (IBAction)synSerial:(id)sender { 19 dispatch_sync(serialQueue, ^{ 20 for (int i = 0 ; i<100; i++) { 21 NSLog(@"%@--task1--%d",[NSThread currentThread],i); 22 } 23 }); 24 dispatch_sync(serialQueue, ^{ 25 for (int i = 0 ; i<100; i++) { 26 NSLog(@"%@--task2--%d",[NSThread currentThread],i); 27 } 28 }); 29 } 30 // 单击“并行同步任务”后执行的行为 31 - (IBAction)synConcurrent:(id)sender{ 32 dispatch_sync(globalQueue, ^{ 33 for (int i = 0 ; i<100; i++) { 34 NSLog(@"%@--task1--%d",[NSThread currentThread],i); 35 } 36 }); 37 dispatch_sync(globalQueue, ^{ 38 for (int i = 0 ; i<100; i++) { 39 NSLog(@"%@--task2--%d",[NSThread currentThread],i); 40 } 41 }); 42 } 43 @end
在例1-4中,第12~15行代码创建了两个队列,分别为串行队列和全局并发队列。第18~29行代码是对串行队列执行同步任务的响应处理,用dispatch_sync()函数以同步的方式调度串行队列的两个代码块。第31~42行代码是对并发队列执行同步任务的响应处理,用dispatch_sync()函数以同步的方式调度并发队列的两个代码块。
(4)程序运行成功后,单击“串行同步任务”按钮,运行结果如图1-25所示。
图1-25 串行队列执行同步任务
从图3-25中看出,任务都是在主线程中执行的,而且必须执行完上一个任务之后,才会开始执行下一个任务。
(5)单击“并行同步任务”按钮,运行结果如图1-26所示。
图1-26 并行队列执行同步任务
从图1-26中看出,任务依然只在主线程中执行,而且是一个一个按顺序执行,这说明采用同步的方式不会开启新的线程。
2. 以异步的方式执行任务
所谓异步执行任务,就是会在新的线程中执行任务,具备开启新线程的能力。当开发者添加一些任务到队列中时,无法确定这些代码什么时候能够执行。通过异步地添加代码块或函数,可以让线程池立即执行这些代码,然后还可以调用线程继续去做其他的事情。开发者应该尽可能地使用dispatch_async()或dispatch_async_f()函数异步地调度任务,这两个函数如下:
void dispatch_async(dispatch_queue_t queue,^(void)block); void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
从上述代码看出,这两个函数都没有返回值,具体传入的参数和同步函数的参数一样,这里就不赘述了,针对它们的介绍如下。
- dispatch_async()函数:将代码块以异步的方式提交给指定队列,该队列底层的线程池将负责执行该代码块。
- dispatch_async_f()函数:将函数以异步的方式提交给指定队列,该队列底层的线程池将负责执行该函数。
需要注意的是,应用程序的主线程一定要异步地调度任务,这样才能及时地响应用户事件。
为了大家更好地掌握,接下来,通过一个简单的案例,讲述如何使用异步的方式向串行队列和并发队列提交任务,具体步骤如下。
(1)新建一个Single View Application应用,名称为05-Dispatch Asyn。
(2)进入Main.StoryBoard,从对象库拖曳两个Button到程序界面,用于控制串行或者并行地执行异步任务,设置两个Button的Title分别为“串行异步任务”和“并行异步任务”。
(3)采用拖曳的方式,为“串行异步任务”按钮和“并行异步任务”按钮添加两个单击响应事件,分别命名为asynSerial:和asynConcurrent:。进入ViewController.m,实现这两个响应按钮单击的方法,如例1-5所示。
【例1-5】ViewController.m
1 #import "ViewController.h" 2 @interface ViewController () 3 - (IBAction)asynSerial:(id)sender; 4 - (IBAction)asynConcurrent:(id)sender; 5 @end 6 @implementation ViewController 7 dispatch_queue_t serialQueue; 8 dispatch_queue_t concurrentQueue; 9 - (void)viewDidLoad { 10 [super viewDidLoad]; 11 // 创建串行队列 12 serialQueue = dispatch_queue_create("cn.itcast", DISPATCH_QUEUE_SERIAL); 13 // 创建并发队列 14 concurrentQueue = dispatch_queue_create("cn.itcast", 15 DISPATCH_QUEUE_CONCURRENT); 16 } 17 // 单击“串行异步任务”后执行的行为 18 - (IBAction)asynSerial:(id)sender { 19 dispatch_async(serialQueue, ^{ 20 for (int i = 0 ; i<100; i++) { 21 NSLog(@"%@--task1--%d",[NSThread currentThread],i); 22 } 23 }); 24 dispatch_async(serialQueue, ^{ 25 for (int i = 0 ; i<100; i++) { 26 NSLog(@"%@--task2--%d",[NSThread currentThread],i); 27 } 28 }); 29 } 30 // 单击“并行异步任务”后执行的行为 31 - (IBAction)asynConcurrent:(id)sender{ 32 dispatch_async(concurrentQueue, ^{ 33 for (int i = 0 ; i<100; i++) { 34 NSLog(@"%@--task1--%d",[NSThread currentThread],i); 35 } 36 }); 37 dispatch_async(concurrentQueue, ^{ 38 for (int i = 0 ; i<100; i++) { 39 NSLog(@"%@--task2--%d",[NSThread currentThread],i); 40 } 41 }); 42 } 43 @end
在例1-5中,第12~15行代码创建了两个队列,分别为串行队列和并发队列。第18~29行代码是对串行队列执行异步任务的响应处理,用dispatch_asyn()函数以异步的方式调度串行队列的两个代码块。第31~42行代码是对并发队列执行异步任务的响应处理,用dispatch_asyn()函数以异步的方式调度并发队列的两个代码块。
(4)程序运行成功后,单击“串行异步任务”按钮,运行结果如图1-27所示。
图1-27 串行队列执行异步任务
从图1-27中看出,线程的number值为2,说明创建了一个子线程。两个任务均是在该线程上被执行的,而且执行完成第1个任务之后才开始执行第2个任务。
(5)单击“并行异步任务”按钮,运行结果如图1-28所示。
图1-28 并发队列执行异步任务
从图1-28看出,这两个任务开启了两个不同的线程,而且任务完成的先后顺序是无法控制的,这表明两个线程是并发执行的,同时也证明了异步任务会开启新的线程。
注意:
针对不同的队列类型,通过同步或者异步的方式会产生各种不同的执行结果,如图1-29所示。
图1-29 各种队列的执行结果
由图1-29可知,同步和异步决定了是否要开启新线程,并发和串行决定了任务的执行方式。
多学一招:Block代码块
Block(块)是Objective-C对ANSI C所做的扩展,使用块可以更好地简化Objective-C编程,而且Objective-C的很多API都依赖于块。接下来,分别从3个方面讲解块的内容,具体内容如下。
(1)块的定义和调用
块的语法格式如下:
^ (块返回值类型) (形参类型1 形参1, 形参类型2 形参2, …) { // 块执行体 }
在上述语法格式中,定义块的语法类似于定义一个函数,但只是定义一个匿名函数。定义代码块与定义函数存在如下差异。
- 定义块必须以“^”开头。
- 定义块的返回值类型是可以省略的,而且经常都会省略声明块的返回值类型。
- 定义块无需指定名字。
- 如果块没有参数,此时参数部分的括号不能省略,但是括号内部可以留空,通常建议使用void作为占位符。
如果程序需要在以后多次调用已经定义的块,那么程序应该将该块赋给一个块变量,定义块 变量的语法格式如下:
块返回值类型(^块变量名) (形参类型1, 形参类型2, ...);
在上述语法格式中,定义块变量时,无需再声明形参名,只要指定形参类型即可。类似的,如果该块不需要形参,则建议使用void作为占位符。
下面通过一个示例代码,演示有参数和无参数两种代码块的定义和调用,代码如例1-6所示。
【例1-6】main.m
1 #import <Foundation/Foundation.h> 2 int main(int argc, const char * argv[]) { 3 @autoreleasepool { 4 // 定义不带参数、无返回值的块 5 void (^printStr) (void) = ^ (void){ 6 NSLog(@"代码块--"); 7 }; 8 // 调用块 9 printStr(); 10 // 定义带参数、有返回值的块 11 int (^sum) (int, int) = ^ (int num1, int num2){ 12 return num1 + num2; 13 }; 14 // 调用块, 输出返回值 15 NSLog(@"%d",sum(10, 15)); 16 // 只定义块变量:带参数、无返回值的块 17 void (^print)(NSString *); 18 // 再将块赋给指定的块变量 19 print = ^ (NSString *str){ 20 NSLog(@"%@", str); 21 }; 22 // 调用块 23 print(@"itcast"); 24 } 25 return 0; 26 }
在例1-6中,第5~7行代码定义了不带参数、无返回值的块,第11~13行代码定义了带参数、有返回值的块,第17~21行代码定义了带参数、无返回值的块,并分别在第9、15、23行代码进行调用,调用块的语法与调用函数完全相同。另外,程序既可以在定义块变量的同时对块变量赋值,也可以先定义块变量,再对块变量赋值。
运行程序,程序的运行结果如图1-30所示。
图1-30 程序的运行结果
(2)块与局部变量
块可以访问程序中局部变量的值,当块访问局部变量的值时,不允许修改该值,如例1-7所示。
【例1-7】main.m
1 #import <Foundation/Foundation.h> 2 int main(int argc, const char * argv[]) { 3 @autoreleasepool { 4 // 定义一个局部变量 5 int a = 20; 6 void (^print) (void) = ^(void){ 7 // 尝试对a赋值 8 a = 30; 9 NSLog(@"%d",a); // 访问局部变量的值是允许的 10 }; 11 // 再次对a赋值 12 a = 40; 13 print(); // 调用块 14 } 15 return 0; 16 }
在例1-7中,第6~10行代码定义了一个块,其中,其8行代码尝试对局部变量a赋值,该行代码引起了Variable is not assignable(missing__block type specifier)错误,下面尝试访问、输出局部变量的值,这是完全允许的。注释第8行代码,再次编译、运行该程序,程序的运行结果如图1-31所示。
图1-31 程序的运行结果
从图1-31中看出,程序最终的输出结果为20,却不是40。这是因为当程序使用块访问局部变量时,系统在定义块时就会把局部变量的值保存在块中,而不是等到执行时才去访问局部变量的值。第12行代码虽然将a变量赋值给40,但是这条语句位于块定义之后,因此,在块定义中a变量的值已经固定为20,后面程序对a变量修改,对块不存在任何影响。
如果希望在定义块时不把局部变量的值复制到块中,而是等到执行时才去访问局部变量的值,甚至希望块也可以改变局部变量的值,这就可以考虑使用__block(两个下划线)修饰局部 变量。对例1-7的代码进行修改,修改后的代码如下:
1 #import <Foundation/Foundation.h> 2 int main(int argc, const char * argv[]) { 3 @autoreleasepool { 4 // 定义__block修饰的局部变量 5 __block int a = 20; 6 void (^print) (void) = ^(void){ 7 // 运行时访问局部变量的值 8 NSLog(@"%d",a); 9 // 尝试对__block修饰的局部变量赋值是允许的 10 a = 30; 11 NSLog(@"%d",a); 12 }; 13 // 再次对a赋值 14 a = 40; 15 print(); // 调用块 16 NSLog(@"块执行完毕后,a的值为%d",a); 17 } 18 return 0; 19 }
在上述代码中,第5行代码定义了一个__block修饰的局部变量a,这表明无论任何时候,块都会直接使用该局部变量本身,而不是将局部变量的值复制到块范围内。运行程序,运行结果如图1-32所示。
图1-32 程序的运行结果
从图1-32中可以看出,当程序调用块时,程序直接访问a变量的值,第8行代码会输出40;当程序执行到第10行代码时,会把a变量本身赋值为30,故第11行代码输出30;当块执行结束以后,程序直接访问a变量的值,故第16行代码输出30。这说明块已经成功地修改了a局部变量的值。
(3)使用typedef定义块变量类型
使用typedef可以定义块变量类型,一旦定义了块变量类型,该块变量主要有如下两个用途:
- 复用块变量类型,即使用块变量类型可以重复定义多个块变量;
- 使用块变量类型定义函数参数,这样即可定义带块参数的函数。
使用typedef定义块变量类型的语法格式如下:
typedef块返回值类型(^块变量类型)(形参类型1, 形参类型2, ...);
下面通过一个示例代码,演示定义块变量类型,再使用该类型重复定义多个变量,代码如例1-8所示。
【例1-8】main.m
1 #import <Foundation/Foundation.h> 2 int main(int argc, const char * argv[]) { 3 @autoreleasepool { 4 // 使用typedef定义块变量类型 5 typedef void (^PrintBlock) (NSString *); 6 PrintBlock print = ^ (NSString *str){ 7 NSLog(@"%@", str); 8 }; 9 // 使用PrintBlock定义块变量,并将指定块赋值给该变量 10 PrintBlock print2 = ^ (NSString *str){ 11 NSLog(@"%@", str); 12 }; 13 // 依次调用两个块 14 print(@"print"); 15 print2(@"print2"); 16 } 17 return 0; 18 }
在例1-8中,第5行代码定义了一个PrintBlock块变量类型,第10行代码复用PrintBlock类型定义变量,这样就可以简化定义块变量的代码。实际上,程序还可以使用该块变量类型定义更多的块变量,只要块变量的形参、返回值类型与此处定义的相同即可。
运行程序,运行结果如图1-33所示。
图1-33 程序的运行结果
除此之外,利用typedef定义的块变量类型可以为函数声明块变量类型的形参,这就要求调用函数时必须传入块变量,示例代码如例1-9所示。
【例1-9】main.m
1 #import <Foundation/Foundation.h> 2 // 定义一个块变量类型 3 typedef void (^ProcessBlock) (int); 4 // 使用ProcessBlock定义最后一个参数类型为块 5 void array(int array[], unsigned int len, ProcessBlock process) 6 { 7 for (int i = 0; i<len; i++) { 8 // 将数组元素作为参数调用块 9 process(array[i]); 10 } 11 } 12 int main(int argc, const char * argv[]) { 13 @autoreleasepool { 14 // 定义一个数组 15 int arr[] = {2, 4, 6}; 16 // 传入块作为参数调用array()函数 17 array(arr, 3, ^(int num) { 18 NSLog(@"元素的平方为:%d", num * num); 19 }); 20 } 21 return 0; 22 }
在例1-9中,第3行代码定义了一个块变量类型,第5行代码使用该块变量类型来声明函数形参,这就要求调用该函数时必须传入块作为参数,第17行代码调用了array()函数,该函数的最后一个参数就是块,这就是直接将块作为函数参数、方法参数的用法。
运行程序,运行结果如图1-34所示。
图1-34 程序的运行结果
1.3.4 实战演练——使用GCD下载图片
前面已经使用NSThread类实现了下载图片,为了大家更好地理解,依然通过一个下载图片的案例,使用GCD来完成多线程的管理,当图片下载完成之后,将图片显示到主线程更新UI,具体步骤如下。
1. 创建工程,设计界面
新建一个Single View Application应用,名称为06-GCDDownload。进入Main.storyboard,从对象库拖曳一个Image View到程序界面,用于放置下载后的图片,设计好的界面如图1-20的左半部分所示。
2. 完成下载图片的功能
单击模拟器的屏幕,通过异步的方式开启子线程来下载图片,当图片从网络上下载完成后,回到主线程显示图片,代码如例1-10所示。
【例1-10】HMViewController.m
1 #define globalQueue 2 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 3 #define mainQueue dispatch_get_main_queue() 4 #import "HMViewController.h" 5 @interface HMViewController () 6 @property (weak, nonatomic) IBOutlet UIImageView *imageView; 7 @end 8 @implementation HMViewController 9 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 10 { 11 dispatch_async(globalQueue, ^{ 12 NSLog(@"donwload---%@", [NSThread currentThread]); 13 //子线程下载图片 14 NSURL *url = [NSURL URLWithString: 15 @"http://www.itcast.cn/images/logo.png"]; 16 NSData *data = [NSData dataWithContentsOfURL:url]; 17 // 将网络数据初始化为UIImage对象 18 UIImage *image = [UIImage imageWithData:data]; 19 if (image != nil) { 20 //回到主线程设置图片 21 dispatch_async(mainQueue, ^{ 22 NSLog(@"setting---%@ %@", [NSThread currentThread], image); 23 self.imageView.image = image; 24 }); 25 } else{ 26 NSLog(@"图片下载出现错误"); 27 } 28 }); 29 } 30 @end
在例1-10中,第1~3行代码表示获取全局并发队列和主队列的宏定义,第11行代码通过一个异步执行的全局并发队列,开启了一个子线程进行图片下载,第21~24行代码将更新UI界面的代码交给了主线程进行。
3. 运行程序
单击左上角的运行按钮,程序运行成功后,单击模拟器屏幕,下载完成的页面如图1-20的右半部分所示。
注意:
为了获取主线程,GCD提供了一个特殊的Dispatch Queue队列,可以在应用的主线程中执行任务。只要应用主线程设置了Run Loop,就会自动创建这个队列,并且最后会自动销毁。对于非Cocoa应用而言,如果没有显式地设置Run Loop,就必须显式地调用dispatch_get_main_queue()函数来激活这个队列。否则,虽然可以添加任务到队列,但任务永远不会被执行。
调用dispatch_get_main_queue()函数可获得应用主线程的Dispatch Queue,添加到这个队列的任务由主线程串行执行。
1.3.5 单次或重复执行任务
在使用GCD时,如果想让某些操作只使用一次,而不重复操作的话,可以使用dispatch_once()函数实现。dispatch_once()函数可以控制提交的代码在整个应用的生命周期内最多执行一次,而且该函数无需传入队列,这就意味着系统将直接使用主线程执行该函数提交的代码块。dispatch_once()函数的定义格式如下所示:
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
该函数需要传入两个参数,第1个参数是一个dispatch_once_t类型的指针,第2个参数是要执行的代码块,第1个参数用于判断第2个参数的代码块是否已经执行过。
在使用GCD时,如果想让某些操作多次重复执行的话,可以使用dispatch_apply()函数来控制提交的代码块重复执行多次。dispatch_apply()函数的定义格式如下所示。
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
该函数需要传入3个参数,第1个参数是任务将要重复执行的次数的迭代,第2个参数是任务要提交的目标队列,第3个参数是要执行的任务代码块,该代码块的size_t参数是该函数第1个参数迭代的具体值。
为了大家更好地理解,接下来,通过一个简单的模拟演练,讲解如何只执行一次任务和重复执行多次任务,具体步骤如下。
(1)新建一个Single View Application应用,命名为07-TaskExecuteTime。
(2)进入 Main.StoryBoard,从对象库拖曳两个 Button到程序界面,并分别设置两个Button的Title为“单次执行”和“重复执行”。
(3)通过拖曳的方式,分别给“单次执行”和“重复执行”按钮添加两个单击事件,分别命名为onceClicked:和moreClicked:。进入ViewController.m,实现这两个响应按钮单击的方法,代码如例1-11所示。
【例1-11】HMViewController.m
1 #import "HMViewController.h" 2 #import "HMImageDownloader.h" 3 @interface HMViewController () 4 // 执行一次 5 - (IBAction)onceClicked:(id)sender; 6 // 重复执行多次 7 - (IBAction)moreClicked:(id)sender; 8 @end 9 @implementation HMViewController 10 - (IBAction)onceClicked:(id)sender 11 { 12 NSLog(@"----touchesBegan"); 13 static dispatch_once_t onceToken; 14 dispatch_once(&onceToken, ^{ 15 NSLog(@"----once-----"); 16 }); 17 } 18 // 重复执行多次 19 - (IBAction)moreClicked:(id)sender 20 { 21 dispatch_queue_t queue = dispatch_get_global_queue( 22 DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 23 dispatch_apply(5, queue, ^(size_t time) { 24 NSLog(@"---执行%lu次---%@",time,[NSThread currentThread]); 25 }); 26 } 27 @end
在例1-11中,第13行代码先创建了一个dispatch_once_t类型的静态变量,该变量用于控制函数中提交的代码块只执行一次。第23行代码使用dispatch_apply()函数控制提交的任务代码块执行5次,该函数所需的代码块可以带一个参数,这个参数表示当前正在执行第几次。
(4)运行程序,运行成功后单击“单次执行”按钮,程序的运行结果如图1-35所示。
图1-35 只执行一次任务
从图1-35中看出,第1次单击“单次执行”按钮,会打印输出字符串“----once-----”,之后再次单击该按钮,这个字符串不再输出,这说明该任务只会被执行一次。
(5)单击“重复执行”按钮,程序的运行结果如图1-36所示。
从图1-36中可以看出,由于程序将代码块提交给了并发队列,该队列分配了4个线程来重复执行该代码块,其中还包括主线程。
图1-36 多次重复执行的任务
1.3.6 调度队列组
假设有一个音乐应用,如果要执行多个下载歌曲的任务,这些耗时的任务会被放到多个线程上异步执行,直到全部的歌曲下载完成,弹出一个提示框来通知用户歌曲已下载完成。
针对这个应用场景,可以考虑使用队列组。一个队列组可以将多个block组成一组,用于监听这一组任务是否全部完成,直到关联的任务全部完成后再发出通知以执行其他的操作。iOS提供了如下函数来使用队列组。
(1)创建队列组
要想使用队列组,首当其冲的就是创建一个队列组对象,可以通过dispatch_group_create()函数来创建,它的定义格式如下:
dispatch_group_t dispatch_group_create(void);
在上述格式中,该函数无需传入任何参数,其返回值是dispatch_group_t类型的。
(2)调度队列组
创建了dispatch_group_t对象后,可以使用dispatch_group_async()函数将block提交至一个队列,同时将这些block添加到一个组里面,函数格式如下:
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block);
在上述格式中,该函数没有返回值,它需要传入3个参数,第1个参数是创建的队列组,第2个参数是将要添加到的队列,第3个参数是将要执行的代码块。需要注意的是,该函数的名称有一个async标志,表示这个组会异步地执行这些代码块。
(3)通知
当全部的任务执行完成之后,通知执行其他的操作,通过dispatch_group_notify()函数来通知,它的定义格式如下:
void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue, dispatch_block_t block);
在上述定义格式中,该函数需要传入3个参数,第1个参数表示创建的队列组,第2个参数表示其他任务要添加到的队列,第3个参数表示要执行的其他代码块。
为了更加深入地理解队列组,接下来,以全局并发队列为例,通过一张图来分析队列组的工作原理,如图1-37所示。
图1-37 队列组的工作原理
从图1-37中看出,将两个耗时的操作放到一个全局并发队列中,同时将这个添加了任务的队列放到队列组中,等到队列组中的所有任务都执行完毕后,才开始执行其他的任务,这样就有效地提高了工作效率,又不会使队列之间相互发生混乱。
为了大家更好地理解,接下来,模拟一个需求,就是从网络上加载两张图片,进行组合后,最终显示到一个Image View上,完成一个图片水印的效果。根据这个需求,通过代码完成相应的逻辑,具体步骤如下。
(1)新建一个single View Application工程,命名为08-Dispatch Group。
(2)进入Main.StoryBoard,从对象库拖曳一个Image View到程序界面,用于显示组合后的图片。
(3)通过拖曳的方式,将Image View在viewController.m文件的类扩展中进行属性的声明。
(4)单击屏幕,依次从网络加载两张图片,直到这两张图片下载完毕,将这两张图片进行组合,最终回到主线程上显示,代码如例1-12所示。
【例1-12】ViewController.m
1 #import "ViewController.h" 2 // 宏定义全局并发队列 3 #define global_queue dispatch_get_global_queue(0, 0) 4 // 宏定义主队列 5 #define main_queue dispatch_get_main_queue() 6 @interface ViewController () 7 @property (weak, nonatomic) IBOutlet UIImageView *imageView; 8 @end 9 @implementation ViewController 10 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 11 { 12 [self groupImage]; 13 } 14 /** 15 * 使用队列组组合图片 16 */ 17 - (void)groupImage 18 { 19 // 1.创建一个队列组和队列 20 dispatch_group_t group = dispatch_group_create(); 21 // 2.下载第1张图片 22 __block UIImage *image1 = nil; 23 dispatch_group_async(group, global_queue, ^{ 24 image1 = [self downloadImage:@"http://g.hiphotos.baidu.com 25 /image/pic/item/f2deb48f8c5494ee460de6182ff5e0fe99257e80.jpg"]; 26 }); 27 // 3.下载第2张图片 28 __block UIImage *image2 = nil; 29 dispatch_group_async(group, global_queue, ^{ 30 image2 = [self downloadImage:@"http://su.bdimg.com 31 /static/superplus/img/logo_white_ee663702.png"]; 32 }); 33 // 4.合并图片 34 dispatch_group_notify(group, global_queue, ^{ 35 // 4.1 开启一个位图上下文 36 UIGraphicsBeginImageContextWithOptions(image1.size, NO, 0.0); 37 // 4.2 绘制第1张图片 38 CGFloat image1W = image1.size.width; 39 CGFloat image1H = image1.size.height; 40 [image1 drawInRect:CGRectMake(0, 0, image1W, image1H)]; 41 // 4.3 绘制第2张图片 42 CGFloat image2W = image2.size.width * 0.3; 43 CGFloat image2H = image2.size.height * 0.3; 44 CGFloat image2Y = image1H - image2H; 45 [image2 drawInRect:CGRectMake(140, image2Y, image2W, image2H)]; 46 // 4.4 得到上下文的图片 47 UIImage *fullImage = UIGraphicsGetImageFromCurrentImageContext(); 48 // 4.5 结束上下文 49 UIGraphicsEndImageContext(); 50 // 4.6 回到主线程显示图片 51 dispatch_async(main_queue, ^{ 52 self.imageView.image = fullImage; 53 }); 54 }); 55 } 56 /** 57 * 封装一个方法,只要传入一个url参数,就返回一张网络上下载的图片 58 */ 59 - (UIImage *)downloadImage:(NSString *)urlStr{ 60 NSURL *imageUrl = [NSURL URLWithString:urlStr]; 61 NSData *data = [NSData dataWithContentsOfURL:imageUrl]; 62 return [UIImage imageWithData:data]; 63 } 64 @end
在例1-12中,第20行代码创建了一个队列组group,第22~32行代码将下载图片的两个block添加到group中,其中,第22、28行代码分别定义了__block修饰的两个属性,这样就能在block中修改变量。
运行程序,程序运行成功后,单击模拟器屏幕,可见第1张人物图片和第2张百度Logo图片组合在一起,形成一张图片显示到屏幕上,实现了水印的效果,如图1-38所示。
图1-38 程序的运行结果