在iOS开发中,谈到多线程,大家第一时间想到的一定是GCD。GCD固然是一套强大的多线程解决方案,能够解决绝大多数的多线程问题,但是他易于上手难于精通且到处是坑的特点也注定了想熟练使用它有一定的难度。而且很多人嘴上天天挂着GCD,实际上对它的实际应用也不甚了解。
再者说,在现在的主流开发模式下,能用到多线程的绝大多数就是网络数据请求和网络图片加载,这两点上AFNetwork+SDWebImage已经能满足几乎所有的需求。而剩下的一小部分,简单好用的NSOperation无疑是比GCD更有优势的。
因此,如果你还是坚持『GCD大法好』,那看到这里就不必再看了。如果你想试一试更简单的方法,那就随我来吧。
什么是NSOperation?
和GCD一样,NSOperation也是苹果提供给我们的一套多线程解决方案。实际上它也是基于GCD开发的,但是比GCD拥有更强的可控性和代码可读性。
NSOperation是一个抽象基类,基本没有什么实际使用价值。我们使用最多的是系统封装好的NSInvocationOperation
和NSBlockOperation
。
不过NSOperation一些通用的方法你要知道
1 NSOperation * operation = [[NSOperation alloc]init]; 2 //开始执行 3 [operation start]; 4 //取消执行 5 [operation cancel]; 6 //执行结束后调用的Block 7 [operation setCompletionBlock:^{ 8 NSLog(@"执行结束"); 9 }];
使用NSInvocationOperation
NSInvocationOperation的使用方式和给Button添加事件比较相似,需要一个对象和一个Selector。使用方法非常简单。
我们先来写一个方法
1 - (void)testNSOperation 2 { 3 NSLog(@"我在第%@个线程",[NSThread currentThread]); 4 }
然后调用它
1 //创建 2 NSInvocationOperation * invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil]; 3 //执行 4 [invo start];
得到这样的执行结果
执行结果
我们可以看到NSInvocationOperation其实是同步执行的,因此单独使用的话,这个东西也没有什么卵用,它需要配合我们后面介绍的NSOperationQueue去使用才能实现多线程调用,所以这里我们只需要记住有这么一个东西就行了。
使用NSBlockOperation
- 终于到了我们今天的第一个重点
NSBlockOperation也是NSOperation的子类,支持并发的实行一个或多个block,使用起来简单又方便
执行以下代码1 NSBlockOperation * blockOperation = [[NSBlockOperation 2 blockOperationWithBlock:^{ 3 NSLog(@"1在第%@个线程",[NSThread currentThread]); 4 }]; 5 [blockOperation addExecutionBlock:^{ 6 NSLog(@"2在第%@个线程",[NSThread currentThread]); 7 }]; 8 [blockOperation addExecutionBlock:^{ 9 NSLog(@"3在第%@个线程",[NSThread currentThread]); 10 }]; 11 [blockOperation addExecutionBlock:^{ 12 NSLog(@"4在第%@个线程",[NSThread currentThread]); 13 }]; 14 [blockOperation addExecutionBlock:^{ 15 NSLog(@"5在第%@个线程",[NSThread currentThread]); 16 }]; 17 [blockOperation addExecutionBlock:^{ 18 NSLog(@"6在第%@个线程",[NSThread currentThread]); 19 }];
这里我们多执行两次并比较结果
第一次执行的结果
第二次执行的结果
第三次执行的结果
- 通过三次不同结果的比较,我们可以看到,NSBlockOperation确实实现了多线程。但是我们可以看到,它并非是将所有的block都放到放到了子线程中。通过上面的打印记录我们可以发现,它会优先将block放到主线程中执行,若主线程已有待执行的代码,就开辟新的线程,但最大并发数为4(包括主线程在内)。如果block数量大于了4,那么剩下的Block就会等待某个线程空闲下来之后被分配到该线程,且依然是优先分配到主线程。
- 另外,同一个block中的代码是同步执行的
为了证明以上猜想,我们为它增加更多block,并给每条block添加两行代码。
1 NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{ 2 NSLog(@"1在第%@个线程",[NSThread currentThread]); 3 NSLog(@"1haha"); 4 }]; 5 [blockOperation addExecutionBlock:^{ 6 NSLog(@"2在第%@个线程",[NSThread currentThread]); 7 NSLog(@"2haha"); 8 }]; 9 [blockOperation addExecutionBlock:^{ 10 NSLog(@"3在第%@个线程",[NSThread currentThread]); 11 NSLog(@"3haha"); 12 }]; 13 [blockOperation addExecutionBlock:^{ 14 NSLog(@"4在第%@个线程",[NSThread currentThread]); 15 NSLog(@"4haha"); 16 }]; 17 [blockOperation addExecutionBlock:^{ 18 NSLog(@"5在第%@个线程",[NSThread currentThread]); 19 NSLog(@"5haha"); 20 }]; 21 [blockOperation addExecutionBlock:^{ 22 NSLog(@"6在第%@个线程",[NSThread currentThread]); 23 NSLog(@"6haha"); 24 }]; 25 [blockOperation addExecutionBlock:^{ 26 NSLog(@"7在第%@个线程",[NSThread currentThread]); 27 NSLog(@"7haha"); 28 }]; 29 [blockOperation addExecutionBlock:^{ 30 NSLog(@"8在第%@个线程",[NSThread currentThread]); 31 NSLog(@"8haha"); 32 }]; 33 [blockOperation addExecutionBlock:^{ 34 NSLog(@"9在第%@个线程",[NSThread currentThread]); 35 NSLog(@"9haha"); 36 }]; 37 [blockOperation addExecutionBlock:^{ 38 NSLog(@"10在第%@个线程",[NSThread currentThread]); 39 NSLog(@"10haha"); 40 }]; 41 42 [blockOperation start];
然后我们看一下执行结果
执行结果
]
- 我们可以看到,最大并发数为4,使用同一个线程的block一定是等待前一个block的代码全部执行结束后才执行,且同步执行。
关于最大并发数
在刚才的结果中我们看到最大并发数为4,但这个值并不是一个固定值。4是我在模拟器上运行的结果,而如果我使用真机来跑的话,最大并发数始终为2。因此,具体的最大并发数和运行环境也是有关系的。我们不必纠结于这个数字
所以NSBlockOperation也不是一个理想的多线程解决方案,尽管我们可以在第一个block中创建UI,在其他Block做数据处理等操作,但还是感觉哪里不舒服。
别着急,我们继续往下看
自定义NSOperation
是的,你没看错,NSOperation是可以自定义的。如果NSInvocationOperation
和NSBlockOperation
无法满足你的需求,你可以选择自定义一个NSOperation。
经过上面的分析,我们发现,系统提供的两种NSOperation是一定满足不了我们的需求的。
那我们是不是需要自定义一个NSOperation呢?
答案是,不需要。
自定义NSOperation并不难,但是依然要写不少代码,这违背了我们简单实现多线程的初衷。况且,接下来我会介绍我们今天真正的主角--NSOperationQueue。所以,我打算直接跳过这一个环节。
NSOPerationQueue
简单使用
终于轮到我们今天的主角了。
顾名思义,NSOperationQueue就是执行NSOperation的队列,我们可以将一个或多个NSOperation对象放到队列中去执行。
比如我们上面介绍过的NSInvocationOperation,我们来将它放到队列中来
1 //依然调用上面的那个方法 2 NSInvocationOperation * invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil]; 3 4 NSOperationQueue * queue = [[NSOperationQueue alloc]init]; 5 [queue addOperation:invo];
看一下执行结果
执行结果
现在它已经被放到子线程中执行了
我们把刚才写的NSBlockOperation也加到这个Queue中来
...原来的代码
1 //[blockOperation start]; 2 [queue addOperation:blockOperation];
然后我们再来看执行情况
执行结果
我们看到,NSInvocationOperation 和 NSBlockOperation是异步执行的,NSBlockOperation中的每一个Block也是异步执行且都在子线程中执行,每一个Block内部也依然是同步执行。
是不是简单好用又强大?
放入队列中的NSOperation对象不需要调用
start
方法,NSOPerationQueue会在『合适』的时机去自动调用
更简单的使用方式
除了上述的将NSOperation添加到队列中的使用方法外,NSOperationQueue提供了一个更加简单的方法,只需以下两行代码就能实现多线程调用
1 NSOperationQueue * queue = [[NSOperationQueue alloc]init]; 2 [queue addOperationWithBlock:^{ 3 //这里是你想做的操作 4 }];
你可以同时添加一个或这个多个Block来实现你的操作
怎么样,是不是简单的要死?
(原来这篇文章只需要看这两句就行了是嘛?????????????????????)
添加依赖关系
如果NSOperationQueue仅能做到这些,那我也不必大费周章了。
NSOperationQueue最吸引人的无疑是它的添加依赖的功能。
举个例子,假如A依赖于B,那么在B执行结束之前,A将永远不会执行
示例代码如下
1 { 2 NSInvocationOperation * op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation1) object:nil]; 3 NSInvocationOperation * op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation2) object:nil]; 4 NSOperationQueue * queue = [[NSOperationQueue alloc]init]; 5 [op2 addDependency:op1]; 6 [queue addOperation:op1]; 7 [queue addOperation:op2]; 8 9 } 10 - (void)testNSInvocationOperation1 11 { 12 NSLog(@"我是op1 我在第%@个线程",[NSThread currentThread]); 13 } 14 - (void)testNSInvocationOperation2 15 { 16 NSLog(@"我是op2 我在第%@个线程",[NSThread currentThread]); 17 }
然后无论你运行多少次,得到的一定是这样的结果
必然结果
这就是依赖关系的好处,op2必定会在op1之后执行,这样会大大方便我们的流程控制。
使用依赖关系有三点需要注意
1.不要建立循环依赖,会造成死锁,原因同循环引用
2.使用依赖建议只使用NSInvocationOperation,NSInvocationOperation和NSBlockOperation混用会导致依赖关系无法正常实现。
3.依赖关系不光在同队列中生效,不同队列的NSOperation对象之前设置的依赖关系一样会生效
2016年03月29日11:16:00更新
之前放的代码有一点小小的问题 添加依赖的代码最好放到添加队列之前
前面说过,NSOperationQueue会在『合适』的时间自动去执行任务,因此你无法确定它到底何时执行,有可能前一秒添加的任务,在你这一秒准备添加依赖的时候就已经执行完了,就会出现依赖无效的假象。代码已更正,谢谢评论区各位提醒
设置优先级
每一个NSOperation的对象都一个queuePriority
属性,表示队列优先级。它是一个枚举值,有这么几个等级可选
优先级可选等级
大家可以去设置试试,不过它并不总是起作用,目前我还没有找到原因。所以还是建议用依赖关系来控制流程。
如果有小伙伴知道怎么让优先级始终生效的办法,请告知我。。。
其他操作及注意事项
NSOperationQueue提供暂停和取消两种操作。
设置暂停只需要设置queue的suspended
属性为YES
或NO
即可
取消你可以选择调用某个NSOperation的cancle
方法,也可以调用Queue的cancelAllOperations
方法来取消全部线程
这里需要强调的是,所谓的暂停和取消并不会立即暂停或取消当前操作,而是不在调用新的NSOperation。
改变queue的maxConcurrentOperationCount可以设置最大并发数。
这里依然有两点需要注意
1.最大并发数是有上限的,即使你设置为100,它也不会超过其上限,而这个上限的数目也是由具体运行环境决定的
2.设置最大并发数一定要在NSOperationQueue初始化后立即设置,因为上面说过,被放到队列中的NSOperation对象是由队列自己决定何时执行的,有可能你这边一添加立马就被执行。因此要想让设置生效一定要在初始化后立即设置
结束语
到这里,NSOperation的知识我们已经介绍完毕,如果你尝试用一两次的话,你一定会爱上他。