iOS多线程之NSOperation<三>

我们在前面两节分别讲了iOS多线程的Pthrea、NSThreadGCD,那么我们关于多线程的学习就剩下最后一个内容,就是NSOperation。
NSOperation NSOperation其实是对GCD的封装,表示了一个独立的计算单元。NSOperation本身是一个抽象类,对我们来说并没有什么实用价值,但是系统帮我们封装了两个它的子类,并且我们也可以自己去封装,它的之类都是用线程安全的方式来建立状态、优先级、依赖性和取消等的模型。
很多执行任务类型的案例都很好的运用了NSOperation,包括网络请求,图像压缩,自然语言处理或者其他很多需要返回处理后数据的、可重复的、结构化的、相对长时间运行的任务。
前面学习GCD的时候我们知道,GCD有两个核心概念就是任务和队列,不能只创建任务,然后置之不理,那样并没有什么用处,NSOperation也是一样的,我们不能仅仅把计算单元做好之后就不管它了,我们还是和GCD一样把它放进一个队列中进行调度,这样我们的计算单元才会运作起来,这时候我们就需要另一个概念:NSOperationQueue。
NSOperationQueue 控制着这些操作的执行,它扮演者任务调度的角色,它总是能在遵循先进先出的原则下‘让高优先级操作’能先于‘低优先级操作’运行,使它管理的操作能基本。
接下来我们先不着急看NSOperation和NSOperationQueue给了我们什么接口,等我们先学习NSOperation有哪些操作可以使用。

状态

NSOperation包含了一个十分优雅的状态机来描述每一个操作的执行。

isReady → isExecuting → isFinished

为了替代不那么清晰的state属性,状态直接由上面那些keypath的KVO通知决定,也就是说,当一个操作在准备好被执行的时候,它发送了一个KVO通知给isReady的keypath,让这个keypath对应的属性isReady在被访问的时候返回YES。

每一个属性对于其他的属性必须是互相独立不同的,也就是同时只可能有一个属性返回YES,从而才能维护一个连续的状态: - isReady: 返回 YES 表示操作已经准备好被执行, 如果返回NO则说明还有其他没有先前的相关步骤没有完成。 - isExecuting: 返回YES表示操作正在执行,反之则没在执行。 - isFinished : 返回YES表示操作执行成功或者被取消了,NSOperationQueue只有当它管理的所有操作的isFinished属性全标为YES以后操作才停止出列,也就是队列停止运行,所以正确实现这个方法对于避免死锁很关键。

取消

早些取消那些没必要的操作是十分有用的。取消的原因可能包括用户的明确操作或者某个相关的操作失败。

与之前的执行状态类似,当NSOperation的-cancel状态调用的时候会通过KVO通知isCancelled的keypath来修改isCancelled属性的返回值,NSOperation需要尽快地清理一些内部细节,而后到达一个合适的最终状态。这个时候isCancelled和isFinished的值将是YES,而isExecuting的值则为NO。

优先级

不可能所有的操作都是一样重要,通过以下的顺序设置queuePriority属性可以加快或者推迟操作的执行:

NSOperationQueuePriorityVeryHigh
NSOperationQueuePriorityHigh
NSOperationQueuePriorityNormal
NSOperationQueuePriorityLow
NSOperationQueuePriorityVeryLow

此外,有些操作还可以指定threadPriority的值,它的取值范围可以从0.0到1.0,1.0代表最高的优先级。鉴于queuePriority属性决定了操作执行的顺序,threadPriority则指定了当操作开始执行以后的CPU计算能力的分配,如果你不知道这是什么,好吧,你可能根本没必要知道这是什么。

依赖性

根据你应用的复杂度不同,将大任务再分成一系列子任务一般都是很有意义的,而你能通过NSOperation
的依赖性实现。
比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作(可能你还会用到这个网络子过程再去下载另一张图片,然后用压缩子过程去压缩磁盘上的图片)。显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络子操作是压缩子操作的依赖,通过代码来说就是:

[resizingOperation addDependency:networkingOperation];//设置resizingOperation依赖networkingOperation
[operationQueue addOperation:networkingOperation];//操作添加进队列
[operationQueue addOperation:resizingOperation];//操作添加进队列

除非一个操作的依赖的isFinished返回YES,不然这个操作不会开始。时时牢记将所有的依赖关系添加到操作队列很重要,不然会像走路遇到一条大沟,就走不过去了哟。
此外,确保不要意外地创建依赖循环,像A依赖B,B又依赖A,这也会导致杯具的死锁。

completionBlock

有一个在iOS 4和Snow Leopard新加入的十分有用的功能就是completionBlock属性。

每当一个NSOperation执行完毕,它就会调用它的completionBlock属性一次,这提供了一个非常好的方式让你能在视图控制器(View Controller)里或者模型(Model)里加入自己更多自己的代码逻辑。比如说,你可以在一个网络请求操作的completionBlock来处理操作执行完以后从服务器下载下来的数据。

对于现在Objective-C程序员必须掌握的工具中,NSOperation依然是最基本的一个。尽管GCD对于内嵌异步操作十分理想,NSOperation依旧提供更复杂、面向对象的计算模型,它对于涉及到各种类型数据、需要重复处理的任务又是更加理想的。在你的下一个项目里使用它吧,让它及带给用户欢乐,你自己也会很开心的。

之前了解过NSOperation的同学们可能看出来了,没错,上面就是翻译Mattt Thompson的,学过iOS的应该都知道这位大神。

下面我们开始学NSOperation和NSOperationQueue给我们提供的接口,然后针对常用接口的用法示例。

执行操作
//如果你子类化NSOperation类,你要重写start方法,如果你直接没有子类化,直接使用start,默认是在主线程执行的。
- (void)start;

//如果你子类化NSOperation类,你可以重写main方法,并且实现它。如果你这样做,你就没有必要调用super。
//你绝不能直接调用mian方法,你应该通过调用start方法启动你的线程。
//如果直接调用好像是会在主线程执行
- (void)main;

//在NSOperation的任务完成之后要执行的方法块
@property(copy) void (^completionBlock)(void);
获取Operation状态
//Operation是否被取消,getter方法用isCancelled
  @property (readonly, getter=isCancelled) BOOL cancelled;

//取消一个Operation
- (void)cancel;

//Operation是否在执行中,getter方法用isExecuting
@property (readonly, getter=isExecuting) BOOL executing;

//Operation操作是否完成,getter方法用isFinished
@property (readonly, getter=isFinished) BOOL finished;

//Operation操作是否是异步的,getter方法用isConcurrent,不建议使用,被下边的asynchronous方法取代了
@property (readonly, getter=isConcurrent) BOOL concurrent;

//同concurrent
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);

//Operation操作是否已经准备就绪,getter方法用isFinished
@property (readonly, getter=isReady) BOOL ready;
依赖关系

依赖关系可以有效的安排操作的顺序

//添加依赖,就是在你添加的依赖Operation完成之后当前Operation才能开始执行
- (void)addDependency:(NSOperation *)op;

//删除一个之前添加的依赖Operation
- (void)removeDependency:(NSOperation *)op;

@property (readonly, copy) NSArray<NSOperation *> *dependencies;
优先级
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
}; //优先级
//NSOperationQueue的优先级,对应上面的枚举
@property NSOperationQueuePriority queuePriority;

//这是线程优先级,已经被弃用了
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);

//这个东西在NSThread的时候已经说过了
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

在NSThread的时候讲的NSQualityOfService,想看就看一下,在最后。

等待NSOperation完成
//知道operation完成之前,后面的代码都不会在执行,
//所以为了避免死锁,在operation启动之前不要调用该方法
- (void)waitUntilFinished;
NSOperationQueue
//添加一个操作
- (void)addOperation:(NSOperation *)op;

//添加一组Operation并且等到数组中的操作全部完成才会继续执行
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);

//快速添加一个operation任务
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);

//队列中的operations
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;

//操作的数量
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);

//最大并发数
@property NSInteger maxConcurrentOperationCount;

//队列挂起,getter方法为isSuspended
@property (getter=isSuspended) BOOL suspended;

//队列名字
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);

//说过了,去看NSThread最那里有写http://www.jianshu.com/p/b1962d8543ca
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

//正在执行的操作的调度队列,默认值是nil,只有在队列中没有正在执行或排队的操作才能被设置这个属性,否则跑出NSInvalidArgumentException异常。
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

//取消队列中的所有操作,正在执行的操作不取消
- (void)cancelAllOperations;

//和Operations的waitUntilFinished方法一样,这个是等待队列中所有的操作完成
- (void)waitUntilAllOperationsAreFinished;

//获取当前的操作队列,只读属性
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);

//获取主队列,只读属性
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);

前面说了系统帮我们封装好了NSOperation的两个子类,下面我们就用这两个子类实现一下NSOperation的常用方法。
终于说到了NSOperation的子类,看一下NSInvocationOperation和NSBlockOperation两个的基本用法。
首先是不配合NSOperationQueue,看一下什么效果

NSLog(@"----");
    NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil]; //创建一个NSInvocationOperation 类型的Operation,绑定funcation方法
    [invocation start];
    NSLog(@"----");
    NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1]; //线程睡眠
        NSLog(@"---NSBlockOperation--%@", [NSThread currentThread]);
    }]; //创建一个NSBlockOperation 类型的Operation,添加块方法
    [block start];

看一下输出结果

2017-08-25 15:27:25.785 NSOperation[2944:455806] ----
2017-08-25 15:27:26.787 NSOperation[2944:455806] ---invocation---<NSThread: 0x60800007bf40>{number = 1, name = main}
2017-08-25 15:27:26.788 NSOperation[2944:455806] ----
2017-08-25 15:27:27.789 NSOperation[2944:455806] ---NSBlockOperation--<NSThread: 0x60800007bf40>{number = 1, name = main}

从输出结果看都是在主线程执行中同步执行的,所以不配合NSOperationQueue并没有什么意义。

NSInvocationOperation
- (void)invocation {
    NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    NSInvocationOperation *invocation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    NSInvocationOperation *invocation3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    //1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:invocation];
    [queue addOperation:invocation2];
    [queue addOperation:invocation3];
}

看一下输出结果

2017-08-25 15:41:34.088 NSOperation[2989:469234] ---invocation---<NSThread: 0x60000007d440>{number = 4, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469216] ---invocation---<NSThread: 0x608000260800>{number = 5, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469218] ---invocation---<NSThread: 0x6080002607c0>{number = 3, name = (null)}

输出结果显示每个Operation都开辟了新线程,并且异步执行,那NSBlockOperation会不会是一样的呢,来看一下吧

- (void)block {
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"1----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"2----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"3----%@",[NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
}

一起看一下输出结果

2017-08-25 15:49:41.484 NSOperation[3013:475949] 1----<NSThread: 0x60000007ea80>{number = 5, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475951] 2----<NSThread: 0x608000070740>{number = 4, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475975] 3----<NSThread: 0x60000007ea00>{number = 3, name = (null)}

和NSInvocationOperation的结果一样呢,那接下来我们看一下还有什么好用的方法吧。
我们看到NSBlockOperation还有个addExecutionBlock方法,这是干嘛的呢?

- (void)block {
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"1----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"2----%@",[NSThread currentThread]);
    }];
    [block2 addExecutionBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"4---%@", [NSThread currentThread]);
    }];
    NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"3----%@",[NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
}

先来看下打印结果再说

2017-08-25 16:10:35.735 NSOperation[3114:492996] 3----<NSThread: 0x608000071180>{number = 5, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493009] 2----<NSThread: 0x6000000711c0>{number = 4, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493008] 1----<NSThread: 0x60800006a500>{number = 3, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:492993] 4---<NSThread: 0x600000071280>{number = 6, name = (null)}

从结果来看似乎和新建一个NSBlockOperation效果一样啊,都是异步执行。
看看队列有中有什么好用的方法吧

//创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:2];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];

输出结果:
2017-08-25 15:53:02.280 NSOperation[3034:479336] 1----<NSThread: 0x60800007b7c0>{number = 3, name = (null)}
2017-08-25 15:53:02.280 NSOperation[3034:479338] 2----<NSThread: 0x600000263d00>{number = 4, name = (null)}
2017-08-25 15:53:03.353 NSOperation[3034:479335] 3----<NSThread: 0x60800007b740>{number = 5, name = (null)}

我们在刚刚的NSBlockOperation测试的基础上加上了一句[queue setMaxConcurrentOperationCount:2],结果变得不一样了呢,3要比1和2晚一秒钟哦,刚好是3的睡眠时间,那就说明3是在1和2完成之后开始的,这就是最大并发量的作用,设置的数字就是它允许开辟的最大操作数数。
最后,我们发现除了刚开始在线程中任务顺序执行外,我们一直没讲串行,因为NSOperationQueue是并行队列,我们想要串行就把最大并发量设置为1就可以了,是不是简单多了,不需要再怕不小心把串行并行写错了。
前面有句话看清楚哦,是最大任务数,不是线程数,看看下面这种情况你就清楚其中的区别了

//创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue setMaxConcurrentOperationCount:1];//设置最大并发量
//    [block1 addDependency:block3];//设置block1依赖block3
    block1.queuePriority = NSOperationQueuePriorityHigh;
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];

输出结果:
2017-08-25 16:44:21.361 NSOperation[3366:527290] 1----<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527290] 4---<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527274] 2----<NSThread: 0x600000267c40>{number = 4, name = (null)}
2017-08-25 16:44:23.482 NSOperation[3366:527274] 3----<NSThread: 0x600000267c40>{number = 4, name = (null)}

所以看时间我们能看到2和4是同一时间执行的,所以setMaxConcurrentOperationCount只能控制人物数量。

前面说了好多遍依赖关系,看一下什么是依赖关系吧

//创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue setMaxConcurrentOperationCount:2];//设置最大并发量
    [block1 addDependency:block3];//设置block1依赖block3
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];

输出结果:
2017-08-25 16:00:12.378 NSOperation[3058:484669] 2----<NSThread: 0x608000267d40>{number = 3, name = (null)}
2017-08-25 16:00:12.378 NSOperation[3058:484671] 3----<NSThread: 0x608000267d80>{number = 4, name = (null)}
2017-08-25 16:00:13.380 NSOperation[3058:484668] 1----<NSThread: 0x60000007ae80>{number = 5, name = (null)}

诶,队列不是先进先出的吗?为什么线程不够的时候不是1和2先执行呢?反而3先执行了。噢,我们设置了1依赖3,所以当cpu要杀1的时候,1说“不行,要杀我,先杀3”,所以先执行了2和3,最后执行1。
也可以不初始化NSBlockOperation,直接在队列中添加block操作哦。

//创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//    [queue setMaxConcurrentOperationCount:1];//设置最大并发量
//    [block1 addDependency:block3];//设置block1依赖block3
    block1.queuePriority = NSOperationQueuePriorityHigh;
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
    
    [queue addOperationWithBlock:^{
        sleep(1);
        NSLog(@"5----%@",[NSThread currentThread]);
    }];

输出结果:
2017-08-25 16:41:30.151 NSOperation[3346:524342] 5----<NSThread: 0x60000007c980>{number = 3, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524343] 1----<NSThread: 0x60000007cb80>{number = 4, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524345] 2----<NSThread: 0x608000071340>{number = 5, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524360] 3----<NSThread: 0x600000075c00>{number = 6, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524359] 4---<NSThread: 0x60800007c780>{number = 7, name = (null)}

看结果和用NSBlockOperation没什么区别哦

最后的最后,关于iOS多线程就到这里了,希望对后学者能有一些帮助,那将是我的荣幸。

推荐阅读更多精彩内容