第1章 多线程编程
学习目标
理解多线程的概念
了解实现多线程的4种方式
掌握线程间的安全和通信
掌握GCD的基本操作
掌握NSOperation的基本操作
应用程序在运行时经常要同时处理多项任务,如一个音乐应用,在播放音乐的同时,用户还可以不停地下载歌曲、搜索歌曲等。也就是说,音乐应用同时进行着播放音乐、下载音乐和接受用户响应等多项任务。系统使用线程对任务进行处理,一条线程同一时间只能处理一个任务。多个任务同时执行就需要多条线程。iOS平台对多线程提供了非常优秀的支持,本章将针对iOS系统中的多线程编程进行详细的讲解。
1.1 多线程概念
由于一条线程同一时间只能处理一个任务,所以一个线程里的任务必须顺序执行。如果遇到耗时操作(如网络通信、耗时运算、音乐播放等),等上一个操作完成再执行下一个任务,则在此时间内用户得不到任何响应,这是很糟糕的用户体验。因此在iOS编程中,通常将耗时操作单独放在一个线程里,而把与用户交互的操作放在主线程里,保证应用及时响应用户的操作,提高用户体验。接下来,将围绕多线程技术进行详细讲解。
1.1.1 多线程概述
多线程是指从软件或者硬件上实现多个线程并发执行的技术。多线程技术使得计算机能够在同一时间执行多个线程,从而提升其整体处理性能。
想要了解多线程,必须先理解进程和线程的概念。进程是指在系统中正在运行的应用程序。这个“运行中的程序”就是一个进程。每个进程拥有着自己的地址空间。
当一个程序进入内存运行时,就会变成一个进程,运行中的每个程序都对应着一个进程,且进程具有一定的独立功能。打开Mac的活动监测器,可以看到当前系统执行的进程,如图1-1所示。
图1-1 Mac系统当前的进程
由图1-1可知,该窗口第1列显示的是“进程名称”,该列的每个应用程序就是一个单独的进程。第4列显示的是当前进程拥有的线程数量,而且不止一条。
进程作为系统进行分配和调度的一个独立单位,它主要包含以下3个主要特征。
(1)独立性
进程是一个能够独立运行的基本单位,它既拥有自己独立的资源,又拥有着自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户的进程是不可以直接访问其他进程的地址空间的。
(2)动态性
进程的实质是程序在系统中的一次执行过程,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,它就具有自己的生命周期和各自不同的状态,进程是动态消亡的。
(3)并发性
多个进程可以在单个处理器中同时执行,而不会相互影响,并发就是同时进行的意思。
需要注意的是,CPU在某一个时间点只能执行一个程序,即只能运行一个单独的进程,CPU会不断地在这些进程之间轮换执行。假如Mac同时运行着QQ、music、PPT、film4个程序,它们在CPU中所对应的进程如图1-2所示。
图1-2展示了CPU在多个进程间切换执行的效果,由于CPU的执行速度相对人的感觉而言太快,造成了多个程序同时运行的假象。如果启动了足够多个程序,CPU就会在这么多个程序间切换,这时用户会明显感觉到程序的运行速度下降。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。一个程序的运行至少要有一个线程,一个进程要想执行任务,必须依靠至少一个线程,这个线程就被称作主线程。线程是进程的基本执行单元,对于大多数应用程序而言,通常只有一个主线程。
当进程被初始化之后,主线程就被创建了,主线程是其他线程最终的父线程,所有界面的显示操作必须在主线程进行。开发者可以创建多个子线程,每条线程之间是相互独立的。假如QQ进程中有3个线程,分别用来处理数据发送、数据显示、数据接收,则它们在进程中的关系如图1-3所示。
图1-2 CPU执行的进程
图1-3 进程与线程
由图1-3可知,一个进程必定有一个主线程,每个线程之间是独立运行的,因此,线程的执行是抢占式的,只有当前的线程被挂起,另一个线程才可以运行。
一个进程中包含若干个线程,这些线程可以利用进程所拥有的资源。通常都是把进程作为分配资源的基本单位,把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,因此对它进行调度所付出的开销就会小得多,能更高效地提高系统内多个程序间并发执行的程度。
当操作系统创建一个进程的时候,必须为进程分配独立的内存空间,并且分配大量的相关资源。但是创建一个线程则要简单得多,因此使用多线程实现并发要比使用多进程性能高很多。总体而言,使用多线程编程有以下优势。
(1)进程间不能共享内存,但是线程之间共享内存是非常容易的。
(2)当硬件处理器的数量有所增加时,程序运行的速度更快,无需做其他任何调整。
(3)充分发挥多核处理器的优势,将不同的任务分配给不同的处理器,真正进入“并行运算”的状态。
(4)将耗时、轮询或者并发需求高的任务分配到其他线程执行,而主线程则负责统一更新界面,这样使得应用程序更加流畅,用户的体验更好。
凡事有利必有弊,多线程既有着一定的优势,同样也有着一定的劣势。如何扬长避短,这是开发者需要注意的问题。多线程的劣势包括以下3方面。
(1)开启线程需要占用一定的内存空间(默认情况下,主线程最大占用1M的栈区空间,子线程最大占用512K的栈区空间),如果要开启大量的线程,势必会占用大量的内存空间,从而降低程序的性能。
(2)开启的线程越多,CPU在调度线程上的开销就会越大,一般最好不要同时开启超过5个线程。
(3)程序的设计会变得更加复杂,如线程之间的通信、多线程间的数据共享等。
1.1.2 线程的串行和并行
如果在一个进程中只有一个线程,而这个进程要执行多个任务,那么这个线程只能一个个地按顺序执行这些任务,也就是说,在同一个时间内,一个线程只能执行一个任务,这样的线程执行方式称为线程的串行。如果在一个进程内要下载文件A、文件B、文件C,则线程的串行执行顺序如图1-4所示。
图1-4中,一个进程中只存在一个线程,该线程需要执行3个任务,每个任务都是下载一个文件。按照先后添加的顺序,只有上一个文件下载完成之后,才会下载下一个文件。
如果一个进程中包含的不止有一条线程,每条线程之间可以并行(同时)执行不同的任务,则称为线程的并行,也称多线程。如果在一个进程内要下载文件A、文件B、文件C,则线程的并行执行顺序如图1-5所示。
图1-4 线程的串行
图1-5 线程的并行
图1-5中,一个进程拥有3个线程,每个线程执行1个任务,3个下载任务没有先后顺序,可以同时执行。
同一时间,CPU只能处理一个线程,也就是只有一个线程在工作。由于CPU快速地在多个线程之间调度,人眼无法感觉到,就造成了多线程并发执行的假象,多线程原理图如图1-6所示。
图1-6 多线程的原理
由图1-6可知,一个进程拥有3个线程,分别为线程A、线程B、线程C。某一时刻,CPU执行线程A为一个小箭头的时间,之后又切换到线程B、线程C,依次类推,这样就形成了多条线程并发执行的效果。
1.1.3 多线程技术种类
iOS提供了4种实现多线程的技术,这些技术各有侧重,既有着一定的优势,也存在着各自的不足,开发者可根据自己的实际情况选择。接下来,通过一张图来描述这4种技术方案,如图1-7所示。
图1-7 多线程技术方案
从图1-7中可以看出,pthread可以实现跨平台多个系统的开发,但是它通过C语言操作,使用难度很大,所以一般不被程序员采用。NSThread是面向对象操作的,通过OC语言来执行操作,但是操作步骤繁多,不易控制,也只是偶尔使用。但是,NSThread的内容有助于初学者理解多线程的本质和实现原理,后面会针对它进行详细的介绍。GCD充分利用了设备的多核优势,用于替代NSThread技术。NSOperation基于GCD,更加面向对象。GCD和NSOperation都是自动管理线程的,在实际开发中更受开发者推崇。后面的小节也会对GCD和NSOperation进行详细的介绍。
1.2 使用NSThread实现多线程
前面已经简单介绍过,NSThread类是实现多线程的一种方案,也是实现多线程的最简单方式。本节将针对NSThread相关的内容进行详细的介绍。
1.2.1 线程的创建和启动
在iOS开发中,通过创建一个NSThread类的实例作为一个线程,一个线程就是一个NSThread对象。要想使用NSThread类创建线程,有3种方法,具体如下所示:
// 1.创建新的线程
- (instancetype)initWithTarget:(id)target selector:(SEL)selector
object:(id)argument
// 2.创建线程后自动启动线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target
withObject:(id)argument;
// 3.隐式创建线程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg;
在上述代码中,这3种方法都是将target对象或者其所在对象的selector方法转化为线程的执行者。其中,selector方法最多可以接收一个参数,而object后面对应的就是它接收的参数。
这3种方法中,第1种方法是对象方法,返回一个NSThread对象,并可以对该对象进行详细的设置,必须通过调用start方法来启动线程;第2种方法是类方法,创建对象成功之后就会直接启动线程,前两个方法没有本质的区别;第3种创建方式属于隐式创建,主要在后台创建线程。
除了以上3种方法,NSThread类还提供了两个方法用于获取当前线程和主线程,具体的定义格式如下:
// 获取当前线程
+ (NSThread *)currentThread;
// 获得主线程
+ (NSThread *)mainThread;
为了大家能够更好地理解,接下来通过一个示例讲解如何运用以上3种方法创建并启动线程,具体步骤如下所示。
(1)新建一个Single View Application应用,名称为01-NSThreadDemo。
(2)进入Main.StoryBoard,从对象库添加一个Button和一个Text View。其中,Button用于响应用户单击事件,而Text View用于测试线程的阻塞,设计好的界面如图1-8所示。
图1-8 设计完成的界面
(3)将StoryBoard上面的Button通过拖曳的方式,在ViewController.m中进行声明以响应btnClick:消息。通过3种创建线程的方法创建线程,这3种方法分别被封装在threadCreate1、threadCreate2、threadCreate3三个方法中,之后依次在btnClick:中被调用,代码如例1-1所示。
【例1-1】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 // 按钮被单击
4 - (IBAction)btnClick:(id)sender;
5 @end
6 @implementation ViewController
7 - (IBAction)btnClick:(UIButton *)sender {
8 // 获取当前线程
9 NSThread *current = [NSThread currentThread];
10 NSLog(@"btnClick--%@--current", current);
11 // 获取主线程
12 NSThread *main = [NSThread mainThread];
13 NSLog(@"btnClick--%@--main", main);
14 [self threadCreate1];
15 }
16 - (void)run:(NSString *)param
17 {
18 // 获取当前线程
19 NSThread *current = [NSThread currentThread];
20 for (int i = 0; i<10; i++) {
21 NSLog(@"%@----run---%@", current, param);
22 }
23 }
24 // 第1种创建方式
25 - (void)threadCreate1{
26 NSThread *threadA = [[NSThread alloc] initWithTarget:self
27 selector:@selector(run:) object:@"哈哈"];
28 threadA.name = @"线程A";
29 // 开启线程A
30 [threadA start];
31 NSThread *threadB = [[NSThread alloc] initWithTarget:self
32 selector:@selector(run:) object:@"哈哈"];
33 threadB.name = @"线程B";
34 // 开启线程B
35 [threadB start];
36 }
37 // 第2种创建方式
38 - (void)threadCreate2{
39 [NSThread detachNewThreadSelector:@selector(run:)
40 toTarget:selfwithObject:@"我是参数"];
41 }
42 //隐式创建线程且启动,在后台线程中执行,也就是在子线程中执行
43 - (void)threadCreate3{
44 [self performSelectorInBackground:@selector(run:) withObject:@"参数3"];
45 }
46 // 测试阻塞线程
47 - (void)test{
48 NSLog(@"%@",[NSThread currentThread]);
49 for (int i = 0; i<10000; i++) {
50 NSLog(@"---------%d", i);
51 }
52 }
53 @end
在例1-1中,第14行代码调用了threadCreate1方法,选择第1种方式创建并启动线程。第25~36行代码创建了threadA和threadB两条线程,并调用start方法开启了线程。线程一旦启动,就会在线程thread中执行self的run:方法,并且将文字“哈哈”作为参数传递给run:方法。程序的运行结果如图1-9所示。
图1-9 第1种方式的运行结果
从图1-9中可以看出,主线程的number值为1,且btnClick操作的当前线程的number值也为1,说明按钮单击事件被系统自动放在主线程中。然后可以看到线程A和线程B并发执行的效果,它们的number值分别为2和3,属于不同子线程。
(4)修改第14行代码为“[self threadCreate2];”,选择第2种方式。第38~41行代码创建了一个线程,线程一旦启动,就会在线程thread中执行self的run:方法,并且将文字“我是参数”作为参数传递给run:方法,程序的运行结果如图1-10所示。
从图1-10中可以看出,创建了一个number值为2的子线程,并且run:方法获取到了“我是参数”这个参数。
图1-10 第2种方式的运行结果
(5)修改第14行代码为“[self threadCreate3];”,隐式创建一个线程。第43~45行代码创建了一个线程,线程一旦启动,就会在线程thread中执行self的run:方法,并且将“参数3”作为参数传递给run:方法,程序的运行结果如图1-11所示。
图1-11 第3种方式的运行结果
从图1-11中可以看出,创建了一个number值为2的子线程,并且run:方法获取到了“参数3”这个参数。
(6)修改第14行代码为“[self test];”,用于测试线程的阻塞情况。重新运行程序,单击按钮后,发现Debug输出栏一直在打印输出,说明线程仍被占用。这时,拖曳屏幕中的文本视图,发现该文本视图没有任何响应。待输出栏停止输出的时候,将输出栏的滚动条滑至顶部,程序的运行结果如图1-12所示。
图1-12 test阻塞运行结果
从图1-12可以看出,test方法执行时所处的线程为主线程,如果把大量耗时的操作放在主线程当中,就会阻塞主线程,影响主线程的其他操作正常响应。
1.2.2 线程的状态
当线程被创建并启动之后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,即便线程开始运行以后,它也不可能一直占用着CPU独自运行。由于CPU需要在多个线程之间进行切换,造成了线程的状态也会在多次运行、就绪之间进行切换,如图1-13所示。
图1-13 线程状态的切换
由图1-13可知,线程主要有5个状态,并按照相应的逻辑顺利地在这几个状态中切换。这些状态的具体介绍如下。
1.新建(New)
当程序新建了一个线程之后,该线程就处于新建状态。这时,它和其他对象一样,仅仅是由系统分配了内存,并初始化了其内部成员变量的值,此时的线程没有任何动态特征。
2.就绪(Runable)
当线程对象调用了start方法后,该线程就处于就绪状态,系统会为其创建方法调用的栈和程序计数器,处于这种状态中的线程并没有开始运行,只是代表该线程可以运行了,但到底何时开始运行,由系统来进行控制。
3.运行(Running)
当CPU调度当前线程的时候,将其他线程挂起,当前线程变成运行状态;当CPU调度其他线程的时候,当前线程处于就绪状态。要测试某个线程是否正在运行,可以调用isExecuting方法,若返回YES,则表示该线程处于运行状态。
4.终止(Exit)
当线程遇到以下3种情况时,线程会由运行状态切换到终止状态,具体如下。
(1)线程执行方法执行完成,线程正常结束。
(2)线程执行的过程中出现了异常,线程崩溃结束。
(3)直接调用NSThread类的exit方法来终止当前正在执行的线程。
若要测试某个线程是否结束,可以调用isFinished方法判断,若返回YES,则表示该线程已经终止。
5.阻塞(Blocked)
如果当前正在执行的线程需要暂停一段时间,并进入阻塞状态,可以通过NSThread类提供的两个类方法来完成,具体定义格式如下:
// 让当前正在执行的线程暂停到date参数代表的时间,并且进入阻塞状态
+ (void)sleepUntilDate:(NSDate *)date;
// 让正在执行的线程暂停ti秒,并且进入阻塞状态。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
需要注意的是,当线程进入阻塞状态之后,在其睡眠的时间内,该线程不会获得执行的机会,即便系统中没有其他可执行的线程,处于阻塞状态的线程也不会执行。
1.2.3 线程间的安全隐患
进程中的一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,这里的资源包括对象、变量、文件等。当多个线程同时访问同一块资源时,会造成资源抢夺,很容易引发数据错乱和数据安全问题。
这里有一个很经典的卖火车票的例子,假设有1000张火车票,同时开启两个窗口执行卖票的动作,每出售一张票后就返回当前的剩余票数,由于两个线程共同抢夺 1000 张的票数资源,容易造成剩余票数混乱,具体如图1-14所示。
图1-14 卖火车票案例
在图1-14所示案例中,两个线程同时读取当前票数是1000,然后线程1的卖票窗口1售出1张票,使票数减1变成999,同时线程2也售出1张票,使票数减1变成999。结果是售出了2张票,但是剩余票数是999,这就造成了数据的错误。为了解决这个问题,实现数据的安全访问,可以使用线程间加锁。针对加锁前后线程A和线程B的变化,分别可用图1-15和图1-16表示。
图1-15 加锁前的示意图
图1-16 加锁后的示意图
图1-16所示是加锁后的示意图,线程中首先对Thread A加了一把锁,第一时间段只有Thread A能够访问资源。当Thread A进行write后解锁,这时,Thread B加了一把锁,第二时间段只有Thread B能够访问资源。这样就能够保证在某一个时刻只能有一个线程访问资源,其他线程无法抢夺资源,既保证了线程间的合理秩序,又避免了线程间抢夺资源造成的混乱。
为了大家更好地理解线程安全的问题,这里引入一个卖票的案例,同时设置3个窗口卖票,模拟为每个窗口开启一个线程,共同访问票数资源。新建一个Single View Application应用,名称为02-ThreadSafeDemo,具体代码如例1-2所示。
【例1-2】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @property (nonatomic, assign) int leftTicketCount; // 剩余票数
4 @end
5 @implementation ViewController
6 // 卖票
7 - (void)saleTickets
8 {
9 while (true) {
10 // 模拟延时
11 [NSThread sleepForTimeInterval:1.0];
12 // 判断是否有票
13 if (self.leftTicketCount > 0) {
14 // 如果有,卖一张
15 self.leftTicketCount--;
16 // 提示余额
17 NSLog(@"%@卖了一张票, 剩余%d张票", [NSThread currentThread].name,
18 self.leftTicketCount);
19 } else { // 如果没有,提示用户
20 NSLog(@"没有余票");
21 return;
22 }
23 }
24 }
25 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
26 {
27 // 总票数
28 self.leftTicketCount = 50;
29 // 创建3个线程,启动后执行saleTickets方法卖票
30 NSThread *t1 = [[NSThread alloc] initWithTarget:self
31 selector:@selector(saleTickets) object:nil];
32 t1.name = @"1号窗口";
33 [t1 start];
34 NSThread *t2 = [[NSThread alloc] initWithTarget:self
35 selector:@selector(saleTickets) object:nil];
36 t2.name = @"2号窗口";
37 [t2 start];
38 NSThread *t3 = [[NSThread alloc] initWithTarget:self
39 selector:@selector(saleTickets) object:nil];
40 t3.name = @"3号窗口";
41 [t3 start];
42 }
43 @end
在例1-2中,该段代码总共创建了3个线程,每个线程都使用saleTickets方法来访问同一个资源,并通过while循环不断减少票数,然后打印剩余票数。当程序运行成功后,单击模拟器的屏幕,控制台的输入如图1-17所示。
图1-17 程序的运行结果
从图1-17中可以看出,由于开启了3个线程执行并发操作,在同一时刻同时抢夺一个资源leftTicketCount,造成了剩余票数统计的混乱。
为了解决这个问题,Objective-C的多线程引入了同步锁的概念,使用@synchronized关键字来修饰代码块,这个代码块可简称为同步代码块,同步代码块的基本格式如下:
@synchronized (obj)
{
// 插入被修饰的代码块
}
在上述语法格式中,obj就是锁对象,添加了锁对象之后,锁对象就实现了对多线程的监控,保证同一时刻只有一个线程执行,当同步代码块执行完成后,锁对象就会释放对同步监视器的锁定。
需要注意的是,虽然Objective-C允许使用任何对象作为同步锁,但是考虑到同步锁存在的意义是阻止多个线程对同一个共享资源的并发访问,因此,同步锁只要一个就可以了。并且同步锁要监视所有线程的整个运行状态,考虑到同步锁的生命周期,通常推荐使用当前的线程所在的控制器作为同步锁。
对例1-2中的saleTickets方法进行修改,修改后的代码如下:
1 - (void)saleTickets{
2 while (true) {
3 // 模拟延时
4 [NSThread sleepForTimeInterval:1.0];
5 // 判断是否有票
6 @synchronized(self) {
7 if (self.leftTicketCount > 0) {
8 // 如果有,卖一张
9 self.leftTicketCount--;
10 // 提示余额
11 NSLog(@"%@卖了一张票, 剩余%d张票", [NSThread
12 currentThread].name,
13 self.leftTicketCount);
14 } else { // 如果没有,提示用户
15 NSLog(@"没有余票");
16 return;
17 }
18 }
19 }
20 }
在上述代码中,第6行代码添加了一个同步锁,它用于将第7~17行执行的代码锁住,执行到第18行代码解锁。其中,第4行代码实现每卖出一张票后让卖票的线程休眠1秒。当程序运行成功后,单击模拟器的屏幕,控制台的运行结果如图1-18所示。
图1-18 程序的运行结果
从图1-18中可以看出,通过给线程加同步锁,成功地实现了线程的同步运行,也就是说,使多条线程按顺序地执行任务。需要注意的是,同步锁会消耗大量的CPU资源,一般的初学者很难把握好性能与功能的平衡,所以在开发中不推荐使用同步锁。
注意: 使用同步锁的时候,要尽量让同步代码块包围的代码范围最小,而且要锁定共享资源的全部读写部分的代码。
1.2.4 线程间的通信
在一个进程中,线程往往不是孤立存在的,多个线程之间要经常进行通信,称为线程间通信。线程间的通信主要体现在,一个线程执行完特定任务后,转到另一个线程去执行任务,在转换任务的同时,将数据也传递给另外一个线程。
NSThread类提供了两个比较常用的方法,用于实现线程间的通信,这两个方法的定义格式如下:
// 在主线程执行方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject: (id)arg
waitUntilDone:(BOOL)wait;
// 在子线程中执行方法
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject: (id)arg
waitUntilDone:(BOOL)wait;
在上述定义的格式中,第1个方法是将指定的方法放在主线程中运行。其中,aSelector就是在主线程中运行的方法,参数arg是当前执行方法所在的线程传递给主线程的参数,参数waitUntilDone是一个布尔值,用来指定当前线程是否阻塞,当为YES的时候会阻塞当前线程,直到主线程执行完毕后才执行当前线程;当为NO的时候,则不阻塞这个线程。第2个方法是创建一个子线程,将指定的方法放在子线程中运行。
为了大家更好地理解,接下来,通过一个使用多线程下载网络图片的案例,讲解如何实现线程间的通信,具体步骤如下。
(1)新建一个Single View Application应用,名称为03-ThreadContact。
(2)进入Main.StoryBoard,从对象库拖曳一个Image View到程序界面,用于显示要下载的图片,设置Mode的模式为Center,最后给Image View设置一个背景颜色。
(3)通过拖曳的方式,将Image View在viewController. m文件的类扩展中进行属性的声明。
(4)单击模拟器的屏幕,开始下载图片,并将下载完成的图片显示到Image View上,这个过程如图1-19所示。
图1-19 下载图片执行顺序图
图1-19展示了下载图片过程中的执行顺序,程序如果直接在主线程中访问网络数据,由于网络速度的不稳定性,一旦网络传输速度较慢时,容易造成主线程的阻塞,从而导致应用程序失去响应。因此,需要将网络下载图片这样耗时的操作放到子线程中,等下载完成后,通知主线程刷新视图,代码如例1-3所示。
【例1-3】ViewController.m
1 #import "HMViewController.h"
2 @interface HMViewController ()
3 @property (weak, nonatomic) IBOutlet UIImageView *imageView;
4 @end
5 @implementation HMViewController
6 // 单击屏幕
7 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
8 {
9 [self performSelectorInBackground:@selector(download) withObject:nil];
10 }
11 // 下载图片
12 - (void)download
13 {
14 NSLog(@"download---%@", [NSThread currentThread]);
15 // 1.图片地址
16 NSString *urlStr = @"http://www.itcast.cn/images/logo.png";
17 NSURL *url = [NSURL URLWithString:urlStr];
18 // 2.根据地址下载图片的二进制数据(这句代码最耗时)
19 NSData *data = [NSData dataWithContentsOfURL:url];
20 // 3.设置图片
21 UIImage *image = [UIImage imageWithData:data];
22 // 4.回到主线程,刷新UI界面(为了线程安全)
23 if(image!=nil){
24 [self performSelectorOnMainThread:@selector(downloadFinished:)
25 withObject:image waitUntilDone:NO];
26 } else{
27 NSLog(@"图片下载出现错误");
28 }
29 }
30 // 下载完成
31 - (void)downloadFinished:(UIImage *)image
32 {
33 self.imageView.image = image;
34 NSLog(@"downloadFinished---%@", [NSThread currentThread]);
35 }
36 @end
在例1-3中,第9行代码调用performSelectorInBackground:withObject:方法创建子线程,并指定了download方法来下载图片,第24行代码调用performSelectorOnMainThread:with Object:waitUntilDone:来到主线程,在主线程中刷新视图。运行程序,程序运行成功后,单击屏幕,就成功下载了图片,如图1-20所示。
同时,控制台的运行结果如图1-21所示。
从图1-21中可以看出,程序执行的download方法是在子线程中执行的,而执行downloadFinished:方法来刷新界面是在主线程中进行的。
图1-20 网络下载图片
图1-21 控制台输出结果
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, …)
{
// 块执行体
}
在上述语法格式中,定义块的语法类似于定义一个函数,但只是定义一个匿名函数。定义代码块与定义函数存在如下差异。 如果程序需要在以后多次调用已经定义的块,那么程序应该将该块赋给一个块变量,定义块变量的语法格式如下:
块返回值类型(^块变量名) (形参类型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 程序的运行结果
1.4 NSOperation和NSOperationQueue
前面已经介绍了实现多线程的几种技术,除了PThread、NSThread、GCD之外,还有一种非常简单的多线程实现方式,就是NSOperation和NSOperationQueue。
NSOpration和NSOperationQueue的实现方式与GCD类似,这是因为它是基于GCD来实现的,不过相比较于GCD而言,NSOpration和NSOperationQueue使用的是OC语言,操作更加面向对象,因此更加容易使用。接下来,本节将针对NSOperation类和NSOperation Queue类的相关内容进行详细的讲解。
1.4.1 NSOperation简介
NSOperation类的实例代表一个多线程任务,这个实例封装了需要执行的操作和执行操作所需的数据,并且能够以并发或非并发的方式执行这个操作。
NSOperation类本身是一个抽象类,一般用于定义子类公用的方法和属性。为了得知任务当前的状态,NSOperation类提供了4个属性来判断,用于回馈它的状态变化,它们的定义格式如下:
@property (readonly, getter=isCancelled) BOOL cancelled; // 取消
@property (readonly, getter=isExecuting) BOOL executing; // 运行
@property (readonly, getter=isFinished) BOOL finished; // 结束
@property (readonly, getter=isReady) BOOL ready; // 就绪
开发者开发时必须处理添加操作的状态,这些状态都是基于KVO通知决定的,所以开发者想要手动改变添加操作的状态时,必须要手动发送通知。这4个属性都是相互独立的,每个时刻只可能有一个状态是YES。其中,finished在操作完成后需要及时设置为YES,这是因为NSOperationQueue所管理的队列中,只有isFinished为YES时才会将该任务从队列中移除,这点在内存管理的时候非常关键,同时这样做也可以有效地避免死锁。
除此之外,NSOperation类还提供了一些常用的方法,用于执行它的实例的操作,如表1-1所示。
表1-1 NSOperation类的常用方法
方法名称 |
功能描述 |
---|---|
- (void)start; |
开启NSOperation对象的执行 |
- (void)main; |
执行非并发的任务,此方法被默认实现,也可以重写此方法来执行多次需要执行的任务 |
- (void)cancel; |
取消当前NSOperation任务 |
- (void)addDependency:(NSOperation *)op; |
添加任务的依赖,当依赖的任务执行完毕后,才会执行当前的任务 |
- (void)removeDependency:(NSOperation *)op; |
取消任务的依赖,依赖的任务关系不会自动消除,必须调用该方法 |
- (void)setCompletionBlock:(void (^)(void)) completion Block; |
当前NSOperation执行完毕后,设置想要执行的操作 |
表1-1列举了NSOperation类一些常见的方法,由表可知,这些方法根据功能的不同,大致可以分为以下操作。
1.执行操作
要想执行一个NSOperation对象,可以通过如下两种方式实现。
(1)第1种是手动地调用start这个方法,这个方法一旦调用,就会在当前调用的线程进行同步执行,因此在主线程中一定要谨慎调用,否则会把主线程阻塞。
(2)第2种是将NSOperation添加到NSOperationQueue中,这是开发者使用最多且被提倡的方法,NSOperationQueue会在NSOperation被添加进去的时候尽快执行,并且实现异步执行。
总而言之,如果只是想将任务实现同步执行,只需要重写main方法,在其内部添加相应的操作;如果想要将任务异步执行,则需要重写start方法,同时让isConcurrent方法返回YES。当把任务添加进NSOperationQueue中时,系统将自动调用重写的这个start方法,这时将不再调用main里面的方法。
2.取消操作
当操作开始执行之后, 默认会一直执行直到完成,但是也可以调用cancel方法中途取消操作的执行。当然,这个操作并非常见的取消,实质上取消操作是按照如下方式作用的:
如果这个操作在队列中没有执行,而这个时候取消这个操作,并将状态finished设置为YES,那么这时的取消就是直接取消了;如果这个操作已经在执行,则只能等待这个操作完成,调用cancel方法也只是将isCancelled的状态设置为YES。
因此,开发者应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。如果是YES,则后面的操作都可以不必再执行了。
3.添加依赖
NSOperation中可以将操作分解为若干个小任务,通过添加它们之间的依赖关系进行操作,就可以对添加的操作设置优先级。例如,最常用的异步加载图片,第1步是通过网络加载图片,第2步可能需要对图片进行处理(调整大小或者压缩保存)。当前的操作通过调用addDependency:方法,可以协调好相应的先后关系。
特别需要注意的是,两个任务间不能添加相互依赖,如A依赖B,同时B又依赖A,这样就会导致死锁。在每个操作完成时,需要将isFinished设置为YES,不然后续的操作是不会开始执行的。
4.监听操作
如果想在一个NSOperation执行完毕后做一些事情,就调用setCompletionBlock:方法来设置想做的事情。
1.4.2 NSOperationQueue简介
NSOperationQueue类的实例代表一个队列,与GCD中的队列一样,同样是先进先出的,它负责管理系统提交的多个NSOperation对象,NSOperationQueue底层维护一个线程池,会按照NSOperation对象添加到队列中的顺序来启动相应的线程。
NSOperationQueue负责管理、执行所有的NSOperation对象,这个对象会由线程池中的线程负责执行。为此,NSOperationQueue提供了一些常见的方法,用于操作队列,如表1-2所示。
表1-2 NSOperationQueue类的常用方法
方法名称 |
功能描述 |
---|---|
- (instancetype)initWithTarget:(id)target selector: (SEL)sel object:(id)arg; |
通过传入指定的参数,初始化一个NSOperation Queue队列,其中sel参数表示要添加的任务所在的方法 |
+ (NSOperationQueue *)currentQueue; |
返回执行当前NSOperation对象的NSOperation Queue队列 |
+ (NSOperationQueue *)mainQueue; |
返回系统主线程的NSOperationQueue队列 |
- (void)addOperation:(NSOperation *)op; |
将NSOperation对象添加到NSOperation Queue队列中 |
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; |
将数组中的NSOperation对象添加NSOperation Queue队列中去 |
- (void)addOperationWithBlock:(void (^)(void)) block; |
向NSOperationQueue队列中添加代码块 |
- (void)cancelAllOperations; |
取消NSOperationQueue队列中所有的正在排队或者执行的NSOperation对象 |
- (void)waitUntilAllOperationsAreFinished; |
阻塞当前线程,直到NSOperation队列中所有的NSOperation对象执行完毕后才解除该阻塞 |
表1-2列举了NSOperationQueue类一些常见的方法,由表可知,这些方法根据功能的不同,大致可以分为以下操作。
1.添加NSOperation到NSOperationQueue中
要想执行任务,需要将要执行的NSOperation对象添加到NSOperationQueue中,由其内部的线程池管理调度。若要添加单个NSOperation对象,可以通过addOperation:方法实现;若想要添加多个NSOperation对象到同一个NSOperationQueue队列中,可以调用如下方法。
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait
从上述代码看出,该方法包含2个参数。其中,第2个参数wait如果设置为YES,将会阻塞当前线程,直到提交的全部NSOperation对象执行完毕后释放;如果设置为NO,该方法会立即返回,NSArray所包含的NSOperation对象将以异步的方式执行,不会阻塞当前线程。
另外,还可以通过block代码块的形式来添加NSOperation对象,可以调用如下方法。
- (void)addOperationWithBlock:(void (^)(void))block
通常情况下,NSOperation对象添加到队列之后,短时间内就会得到运行。如果多个任务间存在依赖,或者整个队列被暂停,则可能需要等待。
需要注意的是,NSOperation对象一旦添加到NSOperationQueue之后,绝对不要再修改NSOperation对象的状态。由于NSOperation对象可能会在任何时候运行,改变它的依赖或数据会产生不利的影响。因此,开发者只能查看NSOperation对象的状态,如是否正在运行、等待运行、已经完成等。
2.修改NSOperation对象的执行顺序
对于添加到队列中的NSOperation对象,它们的执行顺序取决于以下两点。
(1)查看NSOperation对象是否已经就绪,这个是由对象的依赖关系确定的。
(2)根据所有NSOperation对象的相对优先级来确定执行顺序。
为此,NSOperation类提供了queuePriority属性,用于改变添加到队列中的NSOperation对象的优先级,定义格式如下:
@property NSOperationQueuePriority queuePriority;
从上述代码看出,该属性是一个NSOperationQueuePriority类型的变量,这是一个枚举类型。优先级等级由低到高,其表示的意义如下。
(1)NSOperationQueuePriorityVeryLow:非常低。
(2)NSOperationQueuePriorityLow:低。
(3)NSOperationQueuePriorityNormal:一般。
(4)NSOperationQueuePriorityHigh:高。
(5)NSOperationQueuePriorityVeryHigh:非常高。
需要注意的是,优先级只能应用于相同队列中的NSOperation对象,如果应用有多个队列,那么不同队列之间的NSOperation对象的优先级等级是互相独立的。因此,不同队列中优先级低的操作仍然可能比优先级高的操作更早执行。
另外,优先级是不能替代依赖关系的,优先级只是对已经准备好的NSOperation对象确定执行顺序。在执行中先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行。
3.设置或者获取队列的最大并发操作数量
当队列中的线程过多时,显然也会影响到应用程序的执行效率。通过设置队列的最大并发操作数量,可以约束队列中的线程的个数,这样就可以设置队列中最多支持多少个并发线程。
虽然NSOperationQueue类设计用于并发执行操作,但是也可以强制单个队列一次只能执行一个操作。maxConcurrentOperationCount可以配置队列的最大并发操作数量,定义格式如下所示:
@property NSInteger maxConcurrentOperationCount;
当maxConcurrentOperationCount设为1时,就表示队列每次只能执行一个操作,但是串行化的NSOperationQueue并不等同于GCD中的串行Dispatch Queue 。
4.等待NSOperation操作执行完成
在实际开发中,为了优化应用的性能,开发者应该尽可能将应用设计为异步操作,让应用在操作正在执行时可以去处理其他事情。如果需要在当前线程中处理操作之前插入其他操作,通过NSOperation类的waitUntilFinished方法来阻塞当前线程,示例如下:
// 会阻塞当前线程,等到某个NSOperation执行完毕
[operation waitUntilFinished];
从上述代码看出,operation表示一个操作,该操作会等到其余的某个操作执行完毕后再执行。但是应该注意避免编写这样的代码,这不仅影响整个应用的并发性,而且也降低了用户的体验。
另外,NSOperationQueue类提供了一个方法,用于表示某个操作可以在执行的同时等待一个队列中的其他全部操作,示例如下:
// 阻塞当前线程,等待queue的所有操作执行完毕
[queue waitUntilAllOperationsAreFinished];
需要注意的是,在等待一个 queue时,应用的其他线程仍然可以向队列中添加其他操作,因此可能会加长线程的等待时间。绝对不要在应用的主线程中等待一个或者多个NSOperation,而要在子线程中进行等待,否则主线程阻塞将会导致应用无法响应用户事件,应用也将表现为无响应。
5.暂停和继续NSOperationQueue队列
开发者如果想临时暂停NSOperationQueue队列中所有的NSOperation操作的执行,可以将suspended属性设置为YES,定义格式如下:
@property (getter=isSuspended) BOOL suspended;
需要注意的是,暂停一个NSOperationQueue队列不会导致正在执行的NSOperation操作在中途暂停,只是简单地阻止调度新的NSOperation操作执行。可以在响应用户请求时,暂停一个NSOperation操作来暂停等待中的任务,然后根据用户的请求,再次设置suspended属性为NO来继续NSOperationQueue队列中的操作执行。
综上所述,将NSOperation和NSOperationQueue这两个类结合使用,就能够实现多线程,大体分为如下4个步骤。
(1)将需要执行的操作封装到一个NSOperation对象中。
(2)将NSOperation对象添加到NSOperationQueue对象中。
(3)系统自动将NSOperationQueue对象中的NSOperation对象取出来。
(4)将取出的NSOperation对象封装的操作放到一个新线程中执行。
1.4.3 使用NSOperation子类操作
因为NSOperation本身是抽象基类,表示一个独立的计算单元,因此如果要创建对象的话,必须使用它的子类。Foundation框架提供了两个具体子类直接供开发者使用,它们就是NSInvocationOperation和NSBlockOperation类。除此之外,还可以自定义子类,只要继承于NSOperation类,实现内部相应的方法即可。针对这3种情况的详细讲解如下。
1.NSInvocationOperation
NSInvocationOperation类用于将特定对象的特定方法封装成NSOperation,基于一个对象和selector来创建操作。如果已经有现有的方法来执行需要的任务,就可以使用这个类。
接下来,新建一个single View Application工程,命名为09-NSInvocationOperation,具体代码如例1-13所示。
【例1-13】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @end
4 @implementation ViewController
5 - (void)viewDidLoad
6 {
7 [super viewDidLoad];
8 // 创建操作
9 NSInvocationOperation *operation = [[NSInvocationOperation alloc]
10 initWithTarget:self selector:@selector(download) object:nil];
11 // 创建队列
12 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
13 // 添加操作到队列中,会自动异步执行
14 [queue addOperation:operation];
15 }
16 - (void)download
17 {
18 NSLog(@"download-----%@", [NSThread currentThread]);
19 }
20 @end
运行程序,结果如图1-39所示。
图1-39 程序的运行结果
从图1-39中看出,第14行调用addOperation:方法让操作异步执行。若要使操作同步执行,则将第14行代码改为调用start方法开启即可。
2.NSBlockOperation
NSBlockOperation类用于将代码块封装成NSOperation,能够并发地执行一个或者多个block对象,所有相关的block代码块都执行完之后,操作才算完成。
接下来,新建一个single View Application工程,命名为10-NSBlockOperation,具体代码如例1-14所示。
【例1-14】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @end
4 @implementation ViewController
5 - (void)viewDidLoad
6 {
7 [super viewDidLoad];
8 NSBlockOperation *operation = [[NSBlockOperation alloc] init];
9 [operation addExecutionBlock:^{
10 NSLog(@"---下载图片----1---%@", [NSThread currentThread]);
11 }];
12 [operation addExecutionBlock:^{
13 NSLog(@"---下载图片----2---%@", [NSThread currentThread]);
14 }];
15 [operation addExecutionBlock:^{
16 NSLog(@"---下载图片----3---%@", [NSThread currentThread]);
17 }];
18 [operation start];
19 }
20 @end
运行程序,结果如图1-40所示。
图1-40 程序的运行结果
从图1-40中看出,尽管operation对象是通过调用start方法来开启线程的,但是operation添加的3个block是并发执行的,也就是在不同线程中执行的。因此,当同一个操作中的任务量大于1时,该操作会实现异步执行。
3.自定义NSOperation子类
如果NSInvocationOperation和NSBlockOperation对象不能满足需求, 开发者可以自定义操作来直接继承NSOperation,并添加任何想要的行为,这要取决于需要自定义的类是想要实现非并发还是并发的NSOperation。
定义非并发的NSOperation要简单许多,只需要重载main这个方法,在这个方法里面执行主任务,并正确地响应取消事件。对于并发NSOperation操作, 必须重写NSOperation的多个基本方法进行实现。
1.4.4 实战演练——自定义NSOperation子类下载图片
前面已经介绍了NSOperation的两个子类,使用极其方便。如果这两个子类无法满足需求,我们可以自定义一个继承自NSOperation的类。接下来,通过一个下载图片的案例,讲解如何使用自定义的NSOperation子类,这里暂时先介绍非并发的NSOperation,具体内容如下。
(1)新建一个single View Application工程,命名为“11-CustomNSOperation”。
(2)进入Main.StoryBoard,从对象库拖曳1个Image View到程序界面,设置Image View的Mode模式为Center,设置一个背景颜色,并且用拖曳的方式对这个控件进行属性声明。
(3)新建一个类DownloadOperation,继承于NSOperation类,表示下载操作。在DownloadOperation.m文件中,重写main方法,并且为该自定义类创建一个代理,并为该代理提供一个下载图片的方法,DownloadOperation类的声明和实现文件如例1-15和例1-16所示。
【例1-15】DownloadOperation.h
1 #import <Foundation/Foundation.h>
2 #import <UIKit/UIKit.h>
3 @class DownloadOperation;
4 // 定义代理
5 @protocol DownloadOperationDelegate <NSObject>
6 // 下载操作方法
7 - (void)downloadOperation:(DownloadOperation *)operation
8 image:(UIImage *)image;
9 @end
10 @interface DownloadOperation : NSOperation
11 // 需要传入图片的URL
12 @property (nonatomic,strong) NSString *url;
13 // 声明代理属性
14 @property (nonatomic,weak) id<DownloadOperationDelegate> delegate;
15 @end
【例1-16】DownloadOperation.m
1 #import "DownloadOperation.h"
2 @implementation DownloadOperation
3 - (void)main
4 {
5 @autoreleasepool {
6 // 获取下载图片的URL
7 NSURL *url = [NSURL URLWithString:self.url];
8 // 从网络下载图片
9 NSData *data = [NSData dataWithContentsOfURL:url];
10 // 生成图像
11 UIImage *image = [UIImage imageWithData:data];
12 // 在主操作队列通知调用方更新UI
13 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
14 NSLog(@"图片下载完成......");
15 if ([self.delegate respondsToSelector:
16 @selector(downloadOperation:image:)]) {
17 [self.delegate downloadOperation:self image:image];
18 }
19 }];
20 }
21 }
22 @end
在例1-16中,main方法实现了下载操作的功能,并通过downloadOperation:image:方法将下载好的图片通过代理的方式传递给代理方。
(4)在ViewController.m文件中,创建NSOperationQueue队列,设置ViewController成为DownloadOperation的代理对象,创建自定义操作,并将自定义操作对象添加到NSOperationQueue队列中,最后刷新界面,如例1-17所示。
【例1-17】ViewController.m
1 #import "ViewController.h"
2 #import "DownloadOperation.h"
3 @interface ViewController ()<DownloadOperationDelegate>
4 @property (weak, nonatomic) IBOutlet UIImageView *imageView;
5 @end
6 @implementation ViewController
7 - (void)viewDidLoad {
8 [super viewDidLoad];
9 // 创建队列
10 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
11 // 队列添加操作
12 DownloadOperation *operation = [[DownloadOperation alloc] init];
13 operation.delegate = self;
14 operation.url = @"http://www.itcast.cn/images/logo.png";
15 // 将下载操作添加到操作队列中去
16 [queue addOperation:operation];
17 }
18 // 执行下载操作
19 - (void)downloadOperation:(DownloadOperation *)operation image:(UIImage *)image
20 {
21 self.imageView.image = image;
22 }
23 @end
运行程序,运行结果如图1-41所示。
图1-41 程序的运行结果
从图1-41中可以看出,自定义的NSOperation的子类同样实现了图片下载操作。
1.4.5 实战演练——对NSOperation操作设置依赖关系
一个队列中执行任务的先后顺序是不一样的,如果队列的操作是并发执行的,则会创建多个线程,每个操作的优先级更是不固定。通过任务间添加依赖,可以为任务设置执行的先后顺序。为了大家更好地理解,接下来,通过一个案例来演示设置依赖的效果。
新建一个single View Application工程,命名为“12-NSOperationAddDependency”。进入ViewController.m文件,通过一个模拟演示,讲解如何对操作设置依赖关系,代码如例1-18所示。
【例1-18】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @end
4 @implementation ViewController
5 - (void)viewDidLoad {
6 [super viewDidLoad];
7 // 创建队列
8 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
9 // 创建操作
10 NSBlockOperation *operation1 = [NSBlockOperation
11 blockOperationWithBlock:^(){
12 NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]);
13 }];
14 NSBlockOperation *operation2 = [NSBlockOperation
15 blockOperationWithBlock:^(){
16 NSLog(@"执行第2次操作,线程:%@", [NSThread currentThread]);
17 }];
18 NSBlockOperation *operation3 = [NSBlockOperation
19 blockOperationWithBlock:^(){
20 NSLog(@"执行第3次操作,线程:%@", [NSThread currentThread]);
21 }];
22 // 添加依赖
23 [operation1 addDependency:operation2];
24 [operation2 addDependency:operation3];
25 // 将操作添加到队列中去
26 [queue addOperation:operation1];
27 [queue addOperation:operation2];
28 [queue addOperation:operation3];
29 }
30 @end
在例1-18中,第23~24行代码通过对操作设置依赖来改变操作的执行顺序,按照此依赖关系,最先执行operation3,然后执行operation2,最后执行operation1。运行两次程序,两次的运行结果如图1-42和图1-43所示。
图1-42 程序运行的第1次结果
图1-43 程序运行的第2次结果
从图1-42和图1-43中可以看出,队列中的操作执行的先后顺序,确实是按照最先执行operation3,然后执行operation2,最后执行operation1的顺序来的,说明给操作添加依赖关系可以很好地设置操作执行的先后顺序。
1.4.6 实战演练——模拟暂停和继续操作
表视图开启线程下载远程的网络界面,滚动页面时势必会有影响,降低用户的体验。针对这种情况,当用户滚动屏幕的时候,暂停队列;用户停止滚动的时候,继续恢复队列。为了大家更好地理解,接下来,通过一个案例,演示如何暂停和继续操作,具体内容如下。
(1)新建一个single View Application工程,命名为“13-SuspendAndContinue”。
(2)进入Main.StoryBoard,从对象库拖曳3个Button到程序界面,分别设置Title为“添加”“暂停”和“继续”,并且用拖曳的方式给这3个控件进行单击响应的声明,分别对应着添加操作、暂停操作、继续操作。
(3)进入ViewController.m文件,在单击“添加”按钮后激发的方法中,首先设置操作的最大并发操作数为1,向创建的队列中添加20个操作,然后为线程设置休眠时间为1.0s,相当于GCD的异步串行操作。
(4)当队列中的操作正在排队时,则将调用setSuspended:方法传入YES参数将其挂起;当队列中的操作被挂起的时候,则调用setSuspended:方法传入NO参数让它们继续排队,代码如例1-19所示。
【例1-19】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @property (nonatomic,strong) NSOperationQueue *queue;
4 - (IBAction)addOperation:(id)sender;
5 - (IBAction)pause:(id)sender;
6 - (IBAction)resume:(id)sender;
7 @end
8 @implementation ViewController
9 - (void)viewDidLoad {
10 [super viewDidLoad];
11 self.queue = [[NSOperationQueue alloc] init];
12 }
13 // 添加operation
14 - (IBAction)addOperation:(id)sender {
15 // 设置操作的最大并发操作数
16 self.queue.maxConcurrentOperationCount = 1;
17 for (int i = 0; i < 20; i++) {
18 [self.queue addOperationWithBlock:^{
19 // 模拟休眠
20 [NSThread sleepForTimeInterval:1.0f];
21 NSLog(@"正在下载 %@ %d", [NSThread currentThread], i);
22 }];
23 }
24 }
25 // 暂停
26 - (IBAction)pause:(id)sender {
27 // 判断队列中是否有操作
28 if (self.queue.operationCount == 0) {
29 NSLog(@"没有操作");
30 return;
31 }
32 // 如果没有被挂起,才需要暂停
33 if (!self.queue.isSuspended) {
34 NSLog(@"暂停");
35 [self.queue setSuspended:YES];
36 } else{
37 NSLog(@"已经暂停");
38 }
39 }
40 // 继续
41 - (IBAction)resume:(id)sender {
42 // 判断队列中是否有操作
43 if (self.queue.operationCount == 0) {
44 NSLog(@"没有操作");
45 return;
46 }
47 // 如果没有被挂起,才需要暂停
48 if (self.queue.isSuspended) {
49 NSLog(@"继续");
50 [self.queue setSuspended:NO];
51 } else{
52 NSLog(@"正在执行");
53 }
54 }
55 @end
运行程序,程序的运行结果如图1-44所示。
图1-44 程序的运行结果
从图1-44中可以看出,当单击“暂停”按钮后,有一个线程还要继续并执行完毕,这是因为当队列执行暂停的时候,这个线程仍在运行,只有其余排队的线程被挂起。
1.5 本章小结
本章主要介绍了多线程开发的相关知识,首先介绍了线程和进程之间的关系,接着介绍了iOS中几种实现多线程的技术,包括NSThread、GCD、NSOperation和NSOperationQueue,并且针对这3种技术进行了详细的介绍,因为使用PThread和NSThread来管理多线程,线程间的安全和通信比较复杂和难以控制,所以推荐使用GCD和NSOperation来操作管理多线程。但是在实际开发中,对于一般开发者来说,因为有封装好的第三方框架提供使用,所以很少由自己创建多线程,但是了解多线程的使用对开发者来说是必不可少的。
【思考题】
1.简述不同的队列类型通过同步或者异步的方式派发会产生什么结果。
2.简述进程和线程的关系。