iOS -NSOperation并发编程

http://www.cocoachina.com/game/20151201/14517.htmlhtml

 http://blog.csdn.net/qinlicang/article/details/42221585git

本文是投稿文章,做者:RyanJIN(简书
对于iOS的并发编程, 用的最广泛的就是GCD了, GCD结合Block能够so easy的实现多线程并发编程. 但若是你看一些诸如AFNetworking, SDWebImage的源码, 你会发现它们使用的都是NSOperation, 纳尼? 难道NSOperation这货更屌? YES, 它确实更屌! Okay, 那咱们就先来简单PK下GCD和NSOperation(固然这里也包括NSOperationQueue).github

1). NSOperation是基于GCD之上的更高一层封装, 拥有更多的API(e.g. suspend, resume, cancel等等).编程

2). 在NSOperationQueue中, 能够指定各个NSOperation之间的依赖关系.swift

3). 用KVO能够方便的监测NSOperation的状态(isExecuted, isFinished, isCancelled).api

4). 更高的可定制能力, 你能够继承NSOperation实现可复用的逻辑模块.网络

Soga, 原来NSOperation这么拽! Apple官方文档和网络上有不少NSOperation的资料, 但大部分都是很书面化的解释(臣妾看不懂啊%>_<%), 看着看着就云深不知处了. 因此这篇文章我会以灰常通俗的方式来解释NSOperation的并发编程. Okay, let's go!多线程

并发编程的几个概念并发

并发编程简单来讲就是让CPU在同一时间运行多个任务. 这里面有几个容易混淆的概念, 咱们先来一个个的梳理下:异步

1). 串行(Serial) VS. 并行(Concurrent)

串行和并行描述的是任务和任务之间的执行方式. 串行是任务A执行完了任务B才能执行, 它们俩只能顺序执行. 并行则是任务A和任务B能够同时执行.

2). 同步(Synchronous) VS. 异步(Asynchronous)

同步和异步描述的其实就是函数何时返回. 好比用来下载图片的函数A: {download image}, 同步函数只有在image下载结束以后才返回, 下载的这段时间函数A只能搬个小板凳在那儿坐等... 而异步函数, 当即返回. 图片会去下载, 但函数A不会去等它完成. So, 异步函数不会堵塞当前线程去执行下一个函数!

3). 并发(Concurrency) VS. 并行(Parallelism)

这个更容易混淆了, 先用Ray大神的示意图和说明来解释一下: 并发是程序的属性(property of the program), 而并行是计算机的属性(property of the machine).

blob.png

仍是很抽象? 那我再来解释一下, 并行和并发都是用来让不一样的任务能够"同时执行", 只是并行是伪同时, 而并发是真同时. 假设你有任务T1和任务T2(这里的任务能够是进程也能够是线程):

a. 首先若是你的CPU是单核的, 为了实现"同时"执行T1和T2, 那只能分时执行, CPU执行一下子T1后立刻再去执行T2, 切换的速度很是快(这里的切换也是须要消耗资源的, context switch), 以致于你觉得T1和T2是同时执行了(但其实同一时刻只有一个任务占有着CPU).

b. 若是你是多核CPU, 那么恭喜你, 你能够真正同时执行T1和T2了, 在同一时刻CPU的核心core1执行着T1, 而后core2执行着T2, great!

其实咱们日常说的并发编程包括狭义上的"并行"和"并发", 你不能保证你的代码会被并行执行, 但你能够以并发的方式设计你的代码. 系统会判断在某一个时刻是否有可用的core(多核CPU核心), 若是有就并行(parallelism)执行, 不然就用context switch来分时并发(concurrency)执行. 最后再以Ray大神的话结尾: Parallelism requires Concurrency, but Concurrency does not guarantee Parallelism!

并发吧, NSOperation!

NSOperation能够本身独立执行(直接调用[operation start]), 也能够放到NSOperationQueue里面执行, 这两种状况下是否并发执行是不一样的. 咱们先来看看NSOperation独立执行的并发状况.

1. 独立执行的NSOperation

NSOperation默认是非并发的(non-concurrent), 也就说若是你把operation放到某个线程执行, 它会一直block住该线程, 直到operation finished. 对于非并发的operation你只须要继承NSOperation, 而后重写main()方法就妥妥滴了, 好比咱们用非并发的operation来实现一个下载需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation YourOperation 
- (void)main 
{
     @autoreleasepool {
         if  (self.isCancelled)  return ;
         NSData *imageData = [[NSData alloc] initWithContentsOfURL:imageURL];
         if  (self.isCancelled) { imageData = nil;  return ; }
         if  (imageData) {
             UIImage *downloadedImage = [UIImage imageWithData:imageData];
         }
         imageData = nil;
         if  (self.isCancelled)  return ;
         [self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:)                                                                  
                                         withObject:downloadedImage
                                      waitUntilDone:NO];
     }
}
@end

因为NSOperation是能够cancel的, 因此你须要在operation程序内部执行过程当中判断当前operation是否已经被cancel了(isCancelled). 若是已经被cancel那就不往下执行了. 当你在外面调用[operation cancel]后, isCancelled会被置为YES.

NSOperation有三个状态量isCancelled, isExecuting和isFinished. isCancelled上面解释过. main函数执行完成后, isExecuting会被置为NO, 而isFinished则被置为YES.

那肿么实现并发(concurrent)的NSOperation呢? 也很简单:

1). 重写isConcurrent函数, 返回YES, 这个告诉系统各单位注意了我这个operation是要并发的.

2). 重写start()函数.

3). 重写isExecuting和isFinished函数

为何在并发状况下须要本身来设定isExecuting和isFinished这两个状态量呢? 由于在并发状况下系统不知道operation何时finished, operation里面的task通常来讲是异步执行的, 也就是start函数返回了operation不必定就是finish了, 这个你本身来控制, 你何时将isFinished置为YES(发送相应的KVO消息), operation就何时完成了. Got it? Good.

仍是上面那个下载的例子, 咱们用并发的方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (BOOL)isConcurrent {
     return  YES;
}
- (void)start 
{
     [self willChangeValueForKey:@ "isExecuting" ];
     _isExecuting = YES;
     [self didChangeValueForKey:@ "isExecuting" ];
     NSURLRequest * request = [NSURLRequest requestWithURL:imageURL];
     _connection = [[NSURLConnection alloc] initWithRequest:request
                                                   delegate:self];
     if  (_connection == nil) [self finish];
}
- (void)finish
{
     self.connection = nil;
     [self willChangeValueForKey:@ "isExecuting" ];
     [self willChangeValueForKey:@ "isFinished" ];
     _isExecuting = NO;
     _isFinished = YES;
     [self didChangeValueForKey:@ "isExecuting" ];
     [self didChangeValueForKey:@ "isFinished" ];
}
#pragma mark - NSURLConnection delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
     // to do something...
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
     // to do something...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
     [self finish];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
     [self finish];
}
@end

Wow, 并行的operation好像有那么点意思了. 这里面还有几点须要mark一下:

a). operation的executing和finished状态量须要用willChangeValueForKey/didChangeValueForKey来触发KVO消息.

b). 在调用完NSURLConnection以后start函数就返回了, 后面就坐等connection的回调了.

c). 在connection的didFinish或didFail回调里面设置operation的finish状态, 告诉系统operation执行完毕了.

若是你是在主线程调用的这个并发的operation, 那一切都是很是的perfect, 就算你当前在操做UI也不影响operation的下载操做. BUT, 若是你是在子线程调用的, 或者把operation加到了非main queue, 那么问题来了, 你会发现这货的NSURLConnection delegate不走了, what's going on here? 要解释这个问题就要请出另一个武林高手NSRunLoop, Okay, 下面进入NSRunLoop的show time.

Hey, NSRunLoop你是神马东东?

关于NSRunLoop推荐看一下孙源@sunnnyxx的分享视频. 其实从字面上就能够看出来, RunLoop就是跑圈, 保证程序一直在执行. App运行起来以后, 即便你什么都不作, 放在那儿它也不会退出, 而是一直在"跑圈", 这就是RunLoop干的事. 主线程会自动建立一个RunLoop来保证程序一直运行. 但子线程默认不建立NSRunLoop, 因此子线程的任务一旦返回, 线程就over了.

上面的并发operation当start函数返回后子线程就退出了, 当NSURLConnection的delegate回调时, 线程已经木有了, 因此你也就收不到回调了. 为了保证子线程持续live(等待connection回调), 你须要在子线程中加入RunLoop, 来保证它不会被kill掉.

RunLoop在某一时刻只能在一种模式下运行, 更换模式时须要暂停当前的Loop, 而后重启新的Loop. RunLoop主要有下面几个模式:

  • NSDefalutRunLoopMode : 默认Mode, 一般主线程在这个模式下运行

  • UITrackingRunLoopMode : 滑动ScrollView是会切换到这个模式

  • NSRunLoopCommonModes: 包括上面两个模式

这边须要特别注意的是, 在滑动ScrollView的状况下, 系统会自动把RunLoop模式切换成UITrackingRunLoopMode来保证ScrollView的流畅性.

1
2
3
4
5
[NSTimer scheduledTimerWithTimeInterval:1.f
                                  target:self
                                selector:@selector(timerAction:)   
                                userInfo:nil
                                 reports:YES];

当你在滑动ScrollView的时候上面的timer会失效, 缘由是Timer是默认加在NSDefalutRunLoopMode上的, 而滑动ScrollView后系统把RunLoop切换为UITrackingRunLoopMode, 因此timer就不会执行了. 解决方法是把该Timer加到NSRunLoopCommonModes下, 这样即便滑动ScrollView也不会影响timer了.

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

另外还有一个trick是当tableview的cell从网络异步加载图片, 加载完成后在主线程刷新显示图片, 这时滑动tableview会形成卡顿. 一般的思路是tableview滑动的时候延迟加载图片, 等中止滑动时再显示图片. 这里咱们能够经过RunLoop来实现.

1
2
3
4
[self.cellImageView performSelector:@sector(setImage:)
                          withObject:downloadedImage
                          afterDelay:0
                             inModes:@[NSDefaultRunLoopMode]];

当NSRunLoop为NSDefaultRunLoopMode的时候tableview确定中止滑动了, why? 由于若是还在滑动中, RunLoop的mode应该是UITrackingRunLoopMode.

好了, 既然咱们已经了解RunLoop的东东了, 咱们能够回过头来解决上面子线程并发NSOperation下NSURLConnection的Delegate不走的问题, 各位童鞋且继续往下看^_^

呼叫NSURLConnection的异步回调

如今解决方案已经很清晰了, 就是利用RunLoop来监督线程, 让它一直等待delegate的回调. 上面已经说到Main Thread是默认建立了一个RunLoop的, 因此咱们的Option 1是让start函数在主线程运行(即便[operation start]是在子线程调用的).

1
2
3
4
5
6
7
8
9
10
- (void)start 
{
     if  (![NSThread isMainThread]) {
         [self performSelectorOnMainThread:@selector(start)
                                withObject:nil
                             waitUntilDone:NO];
         return ;
     }
     // set up NSURLConnection...
}

或者这样:

1
2
3
4
5
6
- (void)start
{
     [[NSOperationQueue mainQueue] addOperationWithBlock:^{
         self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
     }];
}

这样咱们能够简单直接的使用main run loop, 由于数据delivery是很是快滴. 而后咱们就能够将处理incoming data的操做放到子线程去...

Option 2是让operation的start函数在子线程运行, 可是咱们为它建立一个RunLoop. 而后把URL connection schedule到上面去. 咱们先来瞅瞅AFNetworking是怎么作滴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+ (void)networkRequestThreadEntryPoint:(id)__unused object 
{
     @autoreleasepool {
         [[NSThread currentThread] setName:@ "AFNetworking" ];
         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
         [runLoop run];
     }
}
+ (NSThread *)networkRequestThread 
{
     static NSThread *_networkRequestThread = nil;
     static dispatch_once_t oncePredicate;
     dispatch_once(&oncePredicate, ^{
         _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
         [_networkRequestThread start];
     });
     return  _networkRequestThread;
}
- (void)start 
{
     [self.lock lock];
     if  ([self isCancelled]) {
         [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
     else  if  ([self isReady]) {
         self.state = AFOperationExecutingState;
         [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
     }
     [self.lock unlock];
}

AFNetworking建立了一个新的子线程(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 获取RunLoop对象的时候, 就会建立RunLoop), 而后把它加到RunLoop里面来保证它一直运行.

这边咱们能够简单的判断下当前start()的线程是子线程仍是主线程, 若是是子线程则调用[NSRunLoop currentRunLoop]创新RunLoop, 不然就直接调用[NSRunLoop mainRunLoop], 固然在主线程下就不必调用[runLoop run]了, 由于它原本就是一直run的.

P.S. 咱们还可使用CFRunLoop来启动和中止RunLoop, 像下面这样:

1
2
3
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
                            forMode:NSRunLoopCommonModes];
CFRunLoopRun();

等到该Operation结束的时候, 必定要记得调用CFRunLoopStop()中止当前线程的RunLoop, 让当前线程在operation finished以后能够退出.

2. NSOperationQueue里面执行NSOperation

NSOpertion能够add到NSOperationQueue里面让Queue来触发其执行, 一旦NSOperation被add到Queue里面那么咱们就不care它自身是否是并发设计的了, 由于被add到Queue里面的operation一定是并发的. 并且咱们能够设置Queue的maxConcurrentOperationCount来指定最大的并发数(也就是几个operation能够同时被执行, 若是这个值设为1, 那这个Queue就是串行队列了).

为嘛添加到Queue里面的operation必定会是并发执行的呢? Queue会为每个add到队列里面的operation建立一个线程来运行其start函数, 这样每一个start都分布在不一样的线程里面来实现operation们的并发执行.

重要的事情再强调一遍: 咱们这边所说的并发都是指NSOperation之间的并发(多个operation同时执行), 若是maxConcurrentOperationCount设置为1或者把operation放到[NSOperationQueue mainQueue]里面执行, 那它们只会顺序(Serial)执行, 固然就不可能并发了.

[NSOperationQueue mainQueue]返回的主队列, 这个队列里面任务都是在主线程执行的(固然若是你像AFNetworking同样在start函数建立子线程了, 那就不是在主线程执行了), 并且它会忽略一切设置让你的任务顺序的非并发的执行, 因此若是你把NSOperation放到mainQueue里面了, 那你就放弃吧, 无论你怎么折腾, 它是绝对不会并发滴. 固然, 若是是[[NSOperationQueue alloc] init]那就是子队列(子线程)了.

那...那不对呀, 若是我在子线程调用[operation start]函数, 或者把operation放到非MainQueue里面执行, 可是在operation的内部把start抛到主线程来执行(利用主线程的main run loop), 那多个operation其实不都是要在主线程执行的么, 这样还能并发? Luckily, 仍然是并发执行的(其实我想说的是那必须能并发啊...哈哈).

咱们能够先来看看单线程和多线程下的各个任务(task)的并发执行示意图:

blob.png

Yes! 和上面讨论狭义并发(Concurency)和并行(Parallelism)概念时的理解是同样的, 在单线程状况下(也就是mainQueue的主线程), 各个任务(在咱们这里就是一个个的NSOperation)能够经过分时来实现伪并行(Parallelism)执行.

blob.png

而在多线程状况下, 多个线程同时执行不一样的任务(各个任务也会不停的切换线程)实现task的并发执行.

另外, 咱们在往Queue里面添加operation的时候能够指定它们的依赖关系, 好比[operationB addDependency:operationA], 那么operationB会在operationA执行完毕以后才会执行. 还记得这边"执行完毕(isFinished)"的概念吗? 在并发状况下这个状态量是由你本身设定的, 好比operationA是用来异步下载一张图片, 那么只有图片下载完成以后或者超过timeout下载失败以后, isFinished状态量被标记为YES, 这时Queue才会从队列里面移除operationA, 并启动operationB. 是否是很cool? O(∩_∩)O~~

NSOperation实验课

下面咱们进入实验课啦, 要想真正了解某个东东, 仍是须要打开Xcode, 写上几行代码, 而后Commard+R. 为了帮Apple提高Xcode的使用率:-D, 我会给出几个case, 童鞋们能够本身编写test code来验证:

1). 建立两个operation, 而后直接[operation start], 在NSOperation并发设计和非并发设计的状况下, 查看这两个operation是否同时执行了(最简单的打log看是否是交替打印).

2). 在主线程和子线程下分别调用[operation start], 看看执行状况.

3). 建立operation并放到NSOperationQueue里面执行, 分别看看mainQueue和非mainQueue下的执行状况.

4). maxConcurrentOperationCount设置后的执行状况.

5). 试试NSOperation的依赖关系设置, [operationB addDependency:operationA].

6). 写个完整的demo吧, 好比简单的HTTP Downloader.

最后送上干货Demo, RJHTTPDownloader, 用NSOperation实现的一个下载类. 有的童鞋确定会说用AFNetwroking就能够了, 为嘛要本身去写呢? 这个嘛, 偶是以为别人的代码再怎么看和用都不是你的, 本身动手写的才真正belongs to you! 并且这也不算是重复造轮子, 只是学习轮子是怎么构造的, 这样一步一步的慢慢积累, 总有一天咱们也能写出像AFNetworking这样的代码! 共勉.

相关文章
相关标签/搜索