iOS开发项目化经典教程
上QQ阅读APP看书,第一时间看更新

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所示。

0

图1-22 任务的先进先出原则

从图1-22中可以看出,task1是最先进入队列的,处理完毕后,最先从队列中移除,其余的任务则按照进入队列的顺序依次处理。需要注意的是,由于每个任务的执行时间各不相同,先处理的任务不一定先结束。

根据任务执行方式的不同,队列主要分为两种,分别如下。

(1)Serial Dispatch Queue(串行队列)。

串行队列底层的线程池只有一个线程,一次只能执行一个任务,前一个任务执行完成之后,才能够执行下一个任务,示意图如图1-23所示。

0

图1-23 串行队列

由图1-23可知,串行队列只能有一个线程,一旦task1添加到该队列后,task1就会首先执行,其余的任务等待,直到task1运行结束后,其余的任务才能依次进入处理。

(2)Concurrent Dispatch Queue(并发队列)。

并行队列底层的线程池提供了多个线程,可以按照FIFO的顺序并发启动、执行多个任务,示意图如图1-24所示。

0

图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所示。

0

图1-25 串行队列执行同步任务

从图3-25中看出,任务都是在主线程中执行的,而且必须执行完上一个任务之后,才会开始执行下一个任务。

(5)单击“并行同步任务”按钮,运行结果如图1-26所示。

0

图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所示。

0

图1-27 串行队列执行异步任务

从图1-27中看出,线程的number值为2,说明创建了一个子线程。两个任务均是在该线程上被执行的,而且执行完成第1个任务之后才开始执行第2个任务。

(5)单击“并行异步任务”按钮,运行结果如图1-28所示。

0

图1-28 并发队列执行异步任务

从图1-28看出,这两个任务开启了两个不同的线程,而且任务完成的先后顺序是无法控制的,这表明两个线程是并发执行的,同时也证明了异步任务会开启新的线程。

注意:

针对不同的队列类型,通过同步或者异步的方式会产生各种不同的执行结果,如图1-29所示。

0

图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所示。

0

图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所示。

0

图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所示。

0

图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所示。

0

图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所示。

0

图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所示。

0

图1-35 只执行一次任务

从图1-35中看出,第1次单击“单次执行”按钮,会打印输出字符串“----once-----”,之后再次单击该按钮,这个字符串不再输出,这说明该任务只会被执行一次。

(5)单击“重复执行”按钮,程序的运行结果如图1-36所示。

从图1-36中可以看出,由于程序将代码块提交给了并发队列,该队列分配了4个线程来重复执行该代码块,其中还包括主线程。

0

图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所示。

0

图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所示。

0

图1-38 程序的运行结果