OC之NSOperation
NSOperation与 NSOperationQueue 是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。
1、NSOperation
NSOperation是与单个任务关联的代码和数据的抽象类,任务捆绑在NSOperation实例里,可以简单地认为NSOperation是单个的工作单元。
它在使用上,更加符合面向对象的思想,更加方便的为任务添加依赖关系,同时提供了四个支持KVO监听的代表当前任务执行状态的属性cancelled、executing、finished、ready
。NSOperation
内部对这四个状态行为作了预处理,根据任务的不同状态这四个属性的值会自动改变。
1.1、NSOperation
属性与方法
1.1.1、执行操作
方法 | 方法描述 |
---|---|
-(void)start |
开始执行操作。 |
-(void)main |
执行的非并发任务 |
属性 | 值描述 |
---|---|
completionBlock |
一个读写属性的闭包,当属性finished 的值更改为YES时,将执行该代码块。该代码块通常是一个子线程执行;因此,不应该使用这个块来做刷新UI。一个完成的操作可能因为它被取消或者因为它成功地完成了它的任务而结束;在编写代码块时,应该考虑到这一点。 |
注意: 调用-(void)start
方法之前必须确认操作准备就绪(即ready
的值为YES),否则操作调用-(void)start
会出现异常Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSOperationInternal _start:]: receiver is not yet ready to execute'
:如下图所示
1.1.2、取消操作
方法 | 方法描述 |
---|---|
-(void)cancel |
通知操作对象应该停止执行其任务。该方法不会将操作强制停止;它更新NSOperation 的内部cancelled 以反映状态的变化。如果操作已经完成执行,调用该方法无实际意义;可以调用该方法取消在操作队列中但尚未执行的操作。 |
属性 | 值描述 |
---|---|
cancelled |
一个只读属性的布尔值,指示操作是否已被取消,默认为NO 。调用-(void)cancel 方法将此属性的值设置为YES,一旦取消,操作必须移动到完成状态。 |
1.1.3、获取操作状态
属性 | 值描述 |
---|---|
ready |
一个只读属性的布尔值,指示现在是否可以执行该操作。当为YES时,这个operation是处于执行的就绪状态的,当为NO时,表示它依赖的operation未完成。该属性的默认值为 YES,只有在这个操作执行之前使用- addDependency: 方法给这个操作添加一个依赖,ready 值才会改为 NO;当添加的依赖操作 finished 属性为 YES 即依赖操作完成或者取消时,这个操作的 ready 改为YES。注意: ready 为 NO 的操作还没准备就绪,此时不能调用 - (void)start ,否则异常终止NSInvalidArgumentException 。 |
executing |
一个只读属性的布尔值,指示操作是否正在执行。在实现并发操作对象时,必须重写此属性的实现,以便返回操作的执行状态;对于非并发操作,不需要重新实现此属性。 |
finished |
一个只读属性的布尔值,指示操作是否已完成任务或者被取消。一个操作对象直到finished 属性值变为YES时,才会清除依赖。同样地,操作队列直到finished 属性值为YES时才回将一个NSOperation 移除队列。在实现并发操作对象时,必须重写此属性的实现,以便返回操作的完成状态;对于非并发操作,不需要重新实现此属性。 |
cancelled |
一个只读属性的布尔值,指示操作是否已被取消,默认为NO 。调用-(void)cancel 方法将此属性的值设置为YES,一旦取消,操作必须移动到完成状态。 |
concurrent |
一个只读属性的布尔值,指示操作是否异步执行其任务,默认为NO 。已被asynchronous 属性替代。 |
asynchronous |
一个只读属性的布尔值,指示操作是否异步执行其任务,默认为NO 。在实现异步操作对象时,必须实现此属性并返回YES。 |
name |
一个读写属性的字符串,为操作对象分配一个名称,以方便在调试期间识别它。 |
1.1.4、依赖关系管理
方法 | 方法描述 |
---|---|
-(void)addDependency:(NSOperation *)op |
NSOperation 之间可以设置依赖来保证执行顺序,如一定要让操作A执行完后,才能执行操作B;直到所有依赖的操作都完成执行,该操作才被认为准备好执行。如果该操作已经在执行其任务,那么添加依赖关系没有实际效果。这个方法可以改变此操作的isReady 和dependencies 属性。不能创建循环依赖(不能A依赖于B,B又依赖于A),这样做可能导致操作之间的死锁。 |
-(void)removeDependency:(NSOperation *)op |
移除对指定操作的依赖操作。这个方法可以改变此操作的isReady 和dependencies 属性。 |
属性 | 值描述 |
---|---|
dependencies |
一个只读属性的数组,表示在当前对象开始执行之前必须完成执行的操作对象集合。要向这个数组添加对象,需要使用- (void)addDependency:(NSOperation *)op 方法。在完成执行操作时,不会从这个依赖项列表中删除操作。可以使用这个列表来跟踪所有依赖的操作,包括那些已经完成执行的操作。从列表中删除操作的唯一方法是使用-(void)removeDependency:(NSOperation *)op 方法。 |
1.1.5、配置执行优先级
属性 | 值描述 |
---|---|
queuePriority |
操作队列中操作的执行优先级,这个值用于影响操作退出队列和执行的顺序。如果没有显式设置优先级,该方法返回NSOperationQueuePriorityNormal 。 |
qualityOfService |
NSOperation 对象访问系统资源(如CPU时间、网络资源、磁盘资源等)的优先级,反映有效执行NSOperation 所需的最低服务级别,默认值是NSQualityOfServiceBackground 。服务质量较高的NSOperation 优先于系统资源,因此可以更快地执行任务。想要了解更多可以阅读Energy Efficiency Guide for iOS Apps
|
1.1.6、等待操作对象
-(void)waitUntilFinished
方法:阻塞当前线程的执行,直到操作对象完成其任务;常用于将操作提交到队列后,调用此方法,等待该操作完成。
注意:操作对象永远不能在自身上调用此方法,并且应该避免在提交给与自身相同的操作队列的任何操作上调用它,这样做会导致操作死锁。相反,应用程序的其他部分可以根据需要调用此方法,以防止其他任务在目标操作对象完成之前完成。对于位于不同操作队列中的操作调用此方法通常是安全的,尽管如果每个操作都在另一个操作队列上等待,仍然可以创建死锁。
NSOperation默认是非并发的(non-concurrent);执行它的任务一次,就不能再次执行;
1.2、单独执行一个任务NSOperation
因为NSOperation
是抽象的,所以不能直接使用这个类,可以使用系统定义的子类NSInvocationOperation
/NSBlockOperation
或者我们自定义它的子类。
虽然操作对象通常通过操作对列执行,但是我们也可以手动启动操作队象:
1.2.1、使用NSInvocationOperation
@interface NSInvocationOperation : NSOperation
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;
@property (readonly, retain) NSInvocation *invocation;
@property (nullable, readonly, retain) id result;
@end
我们使用NSInvocationOperation
创建一个任务:
- (void)invocationOperationMethod
{
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationHandleTask:) object:@{}];
invocationOperation.completionBlock = ^{
NSLog(@"监听回调:taskA ------- %@",NSThread.currentThread);
};
[invocationOperation start];
}
- (void)invocationOperationHandleTask:(id)object
{
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
}
运行程序,分析打印结果:
10:09:51 开始处理 -------- <NSThread: 0x60000006a040>{number = 1, name = main}
10:09:51 开始执行:taskA ======= <NSThread: 0x60000006a040>{number = 1, name = main}
10:09:54 结束执行:taskA ------- <NSThread: 0x60000006a040>{number = 1, name = main}
10:09:54 结束处理 -------- <NSThread: 0x60000006a040>{number = 1, name = main}
10:09:54 监听回调:taskA ------- <NSThread: 0x60000047c780>{number = 3, name = (null)}
通过打印结果可以看到:
-
NSInvocationOperation
在当前线程执行任务,会堵塞当前线程直到任务执行完毕。 - 如果想要开辟一条线程执行任务,可以在指定的处理方法里开辟一条线程;当然这样做显得多余,不符合
NSInvocationOperation
的适用场景。 -
NSInvocationOperation
任务完成之后的回调completionBlock
处理在分线程中。
1.2.2、使用NSBlockOperation
我们使用NSBlockOperation
快速创建一个任务,然后使用addExecutionBlock
额外添加了一个任务:
- (void)blockOperationMethod
{
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:6];//模拟耗时任务
NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
}];
blockOperation.completionBlock = ^{
NSLog(@"监听回调: ------- %@",NSThread.currentThread);
};
[blockOperation start];
}
运行程序,分析打印结果:
10:11:52 开始处理 -------- <NSThread: 0x604000074cc0>{number = 1, name = main}
10:11:52 开始执行:taskA ======= <NSThread: 0x604000074cc0>{number = 1, name = main}
10:11:52 开始执行:taskB ======= <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
10:11:55 结束执行:taskA ------- <NSThread: 0x604000074cc0>{number = 1, name = main}
10:11:58 结束执行:taskB ------- <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
10:11:58 结束处理 -------- <NSThread: 0x604000074cc0>{number = 1, name = main}
10:11:58 监听回调: ------- <NSThread: 0x60400047bbc0>{number = 3, name = (null)}
通过打印结果可以看到:
- 相对于
NSInvocationOperation
,NSBlockOperation
可以使用addExecutionBlock
追加多个任务; - 对于
taskA
,NSBlockOperation
在当前线程执行任务;对于taskB
,NSBlockOperation
新开辟一条线程并发执行任务; - 也就是说
NSBlockOperation
封装的操作数 > 1,就会异步并发执行操作;如果 = 1,就会同步执行操作。 -
NSBlockOperation
会堵塞当前线程,直到封装的所有任务处理完毕,当前线程代码才会接着向下执行; -
NSBlockOperation
任务完成之后的回调completionBlock
处理在分线程中。
1.2.3、自定义NSOperation
同步执行
我们已经使用NSOperation
的系统定义子类NSInvocationOperation
与NSBlockOperation
来单独处理耗时任务,知道这两个子类默认在当前线程处理,会堵塞当前线程直到所有任务全部处理完毕,不会开辟新的线程。我们不妨自定义一个NSOperation
的子类,来了解内部的工作机制:
1.2.3.1、非并发类OperationSync
我们定义一个同步执行的OperationSync
操作类,重写- (void)main
方法;可以研究下非并发操作对象的内部实现:
@interface OperationSync : NSOperation
@end
@implementation OperationSync
@synthesize completionBlock = _completionBlock;
- (void)main
{
//该自动释放池可以防止相关线程发生内存泄漏
@autoreleasepool{
//使用 try-catch 语句防止出现超出这个线程范围的异常情况
@try{
//检查操作是否被取消,在取消操作时尽可能快地退出
if (self.isCancelled == NO) {
NSLog(@"耗时任务执行中 ------- %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
if (_completionBlock){
self.completionBlock();
}
}
}@catch (NSException *exception){}
}
}
@end
1.2.3.2、使用OperationSync
类
我们将耗时操作封装在OperationSync
的mian
方法中,
- (void)customSyncOperationTaskAMethod
{
OperationSync *taskA = [[OperationSync alloc] init];
taskA.completionBlock = ^{
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
};
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[taskA start];
}
运行程序,分析打印结果:
10:43:31 开始处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
10:43:31 开始执行:taskA ======= <NSThread: 0x600000076b80>{number = 1, name = main}
10:43:31 耗时任务执行中 ------- <NSThread: 0x600000076b80>{number = 1, name = main}
10:43:34 结束执行:taskA ------- <NSThread: 0x600000076b80>{number = 1, name = main}
10:43:34 结束处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
运行程序,可以看到:
-
OperationSync
同步执行任务,阻塞当前线程; - 由于
completionBlock
回调执行在当前线程,而上文NSInvocationOperation
与NSBlockOperation
的completionBlock
回调都在分线程处理;我们可以推断:苹果新开辟了一条线程用来处理NSInvocationOperation
与NSBlockOperation
的completionBlock
回调;
1.2.4、自定义NSOperation
异步执行
在上文,我们自定义一个NSOperation
的子类OperationSync
来同步处理耗时任务,了解了内部的工作机制。现在我们不妨来实现一个并发执行任务的NSOperation
的子类:
1.2.4.1、并发类OperationAsync
将操作编写为并发,异步执行它,必须添加许多额外功能:
- 重写 start 方法:将该方法更新为以异步方式执行操作,通常通过在新线程调用操作对象的main方法来做到这一点;
- 重写 main 方法(可选):该方法实现与操作关联的任务,也可以直接在 start 方法实现该任务;
- 配置和管理操作的执行环境:并发操作必须设置本身的环境,并向客户端报告其状态。尤其是 isExecuting、 isFinished 和 isAsynchronous 方法必须返回与操作状态有关的值,而且这 3 个方法必须具备线程安全性,当这些值改变时,还必须生成适当的键值观察通知(KVO)。
@interface OperationAsync : NSOperation
@end
@implementation OperationAsync
@synthesize finished = _finished;
@synthesize executing = _executing;
@synthesize completionBlock = _completionBlock;
- (instancetype)init
{
self = [super init];
if (self)
{
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start
{
//检查操作是否被取消,在取消操作时尽可能快地退出
if (self.isCancelled){
//重写start方法,当operation执行完成或者被取消的时候,必须重写这个finished属性以及生成KVO通知
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
//重写start方法,当operation执行完成或者被取消的时候,根据需要调用completionBlock
if (_completionBlock){
self.completionBlock();
}
return;
}
//重写start方法,当operation改变了执行状态时,必须重写这个executing属性以及生成KVO通知。
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main
{
//该自动释放池可以防止相关线程发生内存泄漏
@autoreleasepool{
//使用 try-catch 语句防止出现超出这个线程范围的异常情况
@try{
//检查操作是否被取消,在取消操作时尽可能快地退出
if (self.isCancelled == NO) {
NSLog(@"耗时任务执行中 ------- %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_finished = YES;
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
//重写main方法,当operation执行完成或者被取消的时候,根据需要调用completionBlock
if (_completionBlock){
self.completionBlock();
}
}
}@catch (NSException *exception){}
}
}
- (BOOL)isAsynchronous{
return YES;
}
- (BOOL)isExecuting{
return _executing;
}
- (BOOL)isFinished{
return _finished;
}
@end
1.2.4.2、使用OperationAsync
类
我们使用并发操作OperationAsync
- (void)customAsyncOperationTaskAMethod
{
OperationAsync *taskA = [[OperationAsync alloc] init];
taskA.completionBlock = ^{
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
};
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[taskA start];
}
运行程序,分析打印结果:
10:44:12 开始处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
10:44:12 开始执行:taskA ======= <NSThread: 0x600000076b80>{number = 1, name = main}
10:44:12 结束处理 -------- <NSThread: 0x600000076b80>{number = 1, name = main}
10:44:12 耗时任务执行中 ------- <NSThread: 0x60000027c700>{number = 3, name = (null)}
10:44:15 结束执行:taskA ------- <NSThread: 0x60000027c700>{number = 3, name = (null)}
运行程序,可以看到:
-
OperationAsync
异步执行任务,开辟一条新线程,不会阻塞当前线程; - 由于
completionBlock
在新开辟的分线程被调用,所以回调处理也在分线程被执行;
1.3、NSOperation
生命周期
我们知道NSOperation
有多个操作状态,那么这些操作状态之间有什么联系呢?NSOperation
的生命周期是什么?
我们通过下面程序来探究下:
- (void)operationLifeCycleMethod
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
}];
blockOperationA.name = @"com.demo.taskA";
[blockOperationA addObserver:self forKeyPath:@"ready" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
[blockOperationA addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
[blockOperationA addObserver:self forKeyPath:@"executing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
[blockOperationA addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationA)];
NSBlockOperation *blockOperationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
}];
blockOperationB.name = @"com.demo.taskB";
[blockOperationB addObserver:self forKeyPath:@"ready" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
[blockOperationB addObserver:self forKeyPath:@"cancelled" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
[blockOperationB addObserver:self forKeyPath:@"executing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
[blockOperationB addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(blockOperationB)];
NSLog(@"blockOperationA.ready --- %d",blockOperationA.ready);
NSLog(@"blockOperationB.ready --- %d",blockOperationB.ready);
[blockOperationA addDependency:blockOperationB];
NSLog(@"blockOperationA.ready === %d",blockOperationA.ready);
NSLog(@"blockOperationB.ready === %d",blockOperationB.ready);
[queue addOperation:blockOperationA];
[queue addOperation:blockOperationB];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
id task = (__bridge id)(context);
NSLog(@"%@ ----- %@ === %@",task,keyPath,change[NSKeyValueChangeNewKey]);
}
运行程序,分析打印数据:
11:42:09 开始处理 -------- <NSThread: 0x604000076300>{number = 1, name = main}
11:42:09 blockOperationA.ready --- 1
11:42:09 blockOperationB.ready --- 1
11:42:09 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- ready === 0
11:42:09 blockOperationA.ready === 0
11:42:09 blockOperationB.ready === 1
11:42:09 结束处理 -------- <NSThread: 0x604000076300>{number = 1, name = main}
11:42:09 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- executing === 1
11:42:09 开始执行:taskB ======= <NSThread: 0x60000047cac0>{number = 3, name = (null)}
11:42:11 结束执行:taskB ------- <NSThread: 0x60000047cac0>{number = 3, name = (null)}
11:42:11 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- executing === 1
11:42:11 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- ready === 1
11:42:11 开始执行:taskA ======= <NSThread: 0x60400046d880>{number = 4, name = (null)}
11:42:11 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- executing === 0
11:42:11 <NSBlockOperation: 0x600000441800>{name = 'com.demo.taskB'} ----- finished === 1
11:42:13 结束执行:taskA ------- <NSThread: 0x60400046d880>{number = 4, name = (null)}
11:42:13 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- executing === 0
11:42:13 <NSBlockOperation: 0x6000004401e0>{name = 'com.demo.taskA'} ----- finished === 1
分析打印数据:
- 通过
blockOperation.ready
可以看到,NSOperation
的属性ready
值默认为YES,处于准备就绪状态;当给该NSOperation
添加一个依赖后,属性ready
变为 NO,处于未准备状态。 -
taskB
结束执行后该操作属性finished
变为 YES;这时taskA
操作的属性ready
值改为YES,表示taskA
的操作已经准备就绪,可以开始执行; - 操作
taskA
的属性executing
变为 YES,表示正在执行;taskA
执行完毕之后,executing
变为 NO,finished
变为 1。
我们通过KVO监听操作taskA
的各属性变化,可以得出NSOperation
的生命周期:
1.4、NSOperation
只能start
一次
我们已经知道,NSOperation
的从准备任务到完成任务生命周期历程;那么start
调用一次后,再次调用还能执行任务嘛?答案是否定的!我们不妨来看一段程序:
- (void)startMuchMethod
{
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
}];
[blockOperation start];
NSLog(@"再次执行:taskA ======= %@",NSThread.currentThread);
[blockOperation start];
}
运行程序,分析打印结果:
09:38:10 开始处理 -------- <NSThread: 0x604000062080>{number = 1, name = main}
09:38:10 开始执行:taskA ======= <NSThread: 0x604000062080>{number = 1, name = main}
09:38:12 结束执行:taskA ------- <NSThread: 0x604000062080>{number = 1, name = main}
09:38:12 再次执行:taskA ======= <NSThread: 0x604000062080>{number = 1, name = main}
09:38:12 结束处理 -------- <NSThread: 0x604000062080>{number = 1, name = main}
从打印结果可以看到:NSOperation
的任务只执行了一次,就好比生命只有一次:从出生到死亡的历程。这点不同于dispatch_block_t
,我们dispatch_block_create()
创建了一个dispatch_block_t
,可以多次调用dispatch_block_perform
方法来执行这个任务。
2、操作队列NSOperationQueue
NSOperationQueue 是一个管理操作执行的队列。
当NSOperation
配合NSOperationQueue
使用时,Queue
会监听所有Operation
的状态从而分配任务的启动时机。NSOperation
隐藏了很多内部细节,让开发者无需关心任务的各种状态。
使用 NSOperationQueue 时控制任务数量会并不总是有效,原因何在?利用 NSOperation 封装异步代码有什么需要注意的地方?是否有更好的方法来控制任务的并发数量?为此,我们需要深入了解 NSOperation 的运作机制,现在我们从实际应用场景出发探讨这些问题。
2.1、NSOperationQueue
的属性与方法
2.1.1、访问特定操作队列
属性 | 值描述 |
---|---|
mainQueue |
一个只读属性的NSOperationQueue ,返回绑定到主线程的默认操作队列;返回的队列每次在应用程序的主线程上执行一个操作。(class 修饰,由类对象调用) |
currentQueue |
一个只读属性的NSOperationQueue ,在运行的操作对象中使用此方法来获取当前操作的操作队列。从运行操作的上下文外部调用此方法通常会返回nil。(class 修饰,由类对象调用) |
2.1.2、管理队列中的操作
我们不妨先运行几个程序,体验下操作队列的方法使用
2.1.2.1、程序一
我们使用NSBlockOperation
创建了一个耗时操作;创建了两个操作队列使用NSOperationQueue
的实例方法- addOperation:
将操作添加到这两个操作操作队列:
- (void)addOperationMethod
{
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行 ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:5];//模拟耗时任务
NSLog(@"结束执行 ------- %@",NSThread.currentThread);
}];
[queue1 addOperation:blockOperation];
[queue2 addOperation:blockOperation];
}
运行这段代码,发现程序异常终止:-[NSOperationQueue addOperation:]: operation is already enqueued on a queue'
操作已经加入到队列中:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSOperationQueue addOperation:]: operation is already enqueued on a queue'
*** First throw call stack:
(
0 CoreFoundation 0x00000001052e21e6 __exceptionPreprocess + 294
1 libobjc.A.dylib 0x0000000104977031 objc_exception_throw + 48
2 CoreFoundation 0x0000000105357975 +[NSException raise:format:] + 197
3 Foundation 0x0000000104386f4c __addOperations + 1186
4 OperationQueueDemo 0x00000001040632c4 -[OperationQueueTableViewController addOperationMethod] + 212
5 OperationQueueDemo 0x00000001040618c7 -[OperationQueueTableViewController tableView:didSelectRowAtIndexPath:] + 583
6 UIKit 0x0000000106253e89 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1813
7 UIKit 0x00000001062540a4 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 344
8 UIKit 0x00000001061204b3 _runAfterCACommitDeferredBlocks + 318
9 UIKit 0x000000010610f71e _cleanUpAfterCAFlushAndRunDeferredBlocks + 388
10 UIKit 0x000000010613dea5 _afterCACommitHandler + 137
11 CoreFoundation 0x0000000105284607 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
12 CoreFoundation 0x000000010528455e __CFRunLoopDoObservers + 430
13 CoreFoundation 0x0000000105268b81 __CFRunLoopRun + 1537
14 CoreFoundation 0x000000010526830b CFRunLoopRunSpecific + 635
15 GraphicsServices 0x000000010b54ea73 GSEventRunModal + 62
16 UIKit 0x0000000106115057 UIApplicationMain + 159
17 OperationQueueDemo 0x000000010405dfaf main + 111
18 libdyld.dylib 0x00000001096f5955 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
- (void)addOperation:(NSOperation *)op
将指定的操作添加到操作队列。如果操作已经在另一个队列中,该方法将抛出一个NSInvalidArgumentException异常
。
还记得上文提过的NSOperation
在ready
状态为NO时调用start
方法导致程序抛出NSInvalidArgumentException
异常嘛?类似地,如果NSOperation
正在执行(executing
为YES)或已经执行完毕(finished
为YES),-addOperation:
方法也会抛出NSInvalidArgumentException
异常。
异常终止:operation is executing and cannot be enqueued
正在执行操作,无法加入队列.png异常终止:operation is finished and cannot be enqueued
操作已经完成,无法加入队列.png2.1.2.2、程序二
我们使用NSBlockOperation
创建了操作A,操作B,操作C,操作D;使用NSOperationQueue
的实例方法- addOperation:
将操作A、操作C、操作D添加到操作队列,使用- addOperations: waitUntilFinished:
方法将操作B添加至操作队列;在添加操作C与操作D之间使用- waitUntilAllOperationsAreFinished
堵塞当前线程。
- (void)waitUntilFinishedMethod
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskA ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:5];//模拟耗时任务
NSLog(@"结束执行:taskA ------- %@",NSThread.currentThread);
}];
blockOperationA.name = @"com.demo.taskA";
NSBlockOperation *blockOperationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskB ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行:taskB ------- %@",NSThread.currentThread);
}];
blockOperationB.name = @"com.demo.taskB";
NSBlockOperation *blockOperationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskC ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行:taskC ------- %@",NSThread.currentThread);
}];
blockOperationC.name = @"com.demo.taskC";
NSBlockOperation *blockOperationD = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"开始执行:taskD ======= %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行:taskD ------- %@",NSThread.currentThread);
}];
blockOperationD.name = @"com.demo.taskD";
[queue addOperation:blockOperationA];
[queue addOperations:@[blockOperationB] waitUntilFinished:YES];
NSLog(@"queue.operations === 1 === %@",queue.operations);
[queue addOperation:blockOperationC];
NSLog(@"queue.operations === 2 === %@",queue.operations);
[queue waitUntilAllOperationsAreFinished];
[queue addOperation:blockOperationD];
NSLog(@"queue.operations === 3 === %@",queue.operations);
}
我们运行程序,分析打印数据:
10:21:58 开始处理 -------- <NSThread: 0x60400006f6c0>{number = 1, name = main}
10:21:58 开始执行:taskA ======= <NSThread: 0x60000046bac0>{number = 3, name = (null)}
10:21:58 开始执行:taskB ======= <NSThread: 0x60400046bd80>{number = 4, name = (null)}
10:22:01 结束执行:taskB ------- <NSThread: 0x60400046bd80>{number = 4, name = (null)}
10:22:01 queue.operations === 1 === (
"<NSBlockOperation: 0x60000044f900>{name = 'com.demo.taskA'}"
)
10:22:01 queue.operations === 2 === (
"<NSBlockOperation: 0x60000044f900>{name = 'com.demo.taskA'}",
"<NSBlockOperation: 0x6000002586c0>{name = 'com.demo.taskC'}"
)
10:22:01 开始执行:taskC ======= <NSThread: 0x60400046bd80>{number = 4, name = (null)}
10:22:03 结束执行:taskA ------- <NSThread: 0x60000046bac0>{number = 3, name = (null)}
10:22:04 结束执行:taskC ------- <NSThread: 0x60400046bd80>{number = 4, name = (null)}
10:22:04 queue.operations === 3 === (
"<NSBlockOperation: 0x6000004436c0>{name = 'com.demo.taskD'}"
)
10:22:04 开始执行:taskD ======= <NSThread: 0x60000046bac0>{number = 3, name = (null)}
10:22:04 结束处理 -------- <NSThread: 0x60400006f6c0>{number = 1, name = main}
10:22:07 结束执行:taskD ------- <NSThread: 0x60000046bac0>{number = 3, name = (null)}
通过打印数据:
-
taskB
执行完毕之后,才开始执行taskC
:这是因为- addOperations: waitUntilFinished:
方法的第二个参数wait
如果YES
,则阻塞当前线程,直到所有指定的操作执行完毕;如果为NO
,则不会堵塞当前线程,立即返回。 -
taskC
执行完毕之后,才调用结束处理
这句代码:这是因为- waitUntilAllOperationsAreFinished
方法会阻塞当前线程,直到队列中所有操作执行完成为止;在此期间,当前线程不能向队列添加操作,但其他线程可以。 - 通过
queue.operations === 1 ===
可以看到此时的操作队列只有操作com.demo.taskA
:这是因为- addOperations: waitUntilFinished:
方法的第二个参数wait
如果YES
,则阻塞当前线程,直到指定的操作执行完毕,这时会将指定的操作移除操作队列; - 通过
queue.operations === 2 ===
与queue.operations === 3 ===
的结果对比,可以看到- waitUntilAllOperationsAreFinished
之前的操作都被移除操作队列。
思考一下: - waitUntilAllOperationsAreFinished
方法已将操作从队列中移除,这时调用-addOperation:
方法将该操作添加到另一个队列是否会抛出异常?为什么?
方法 | 方法描述 |
---|---|
- (void)addOperation:(NSOperation *)op |
将指定的操作添加到操作队列。添加后,指定的操作将保留在操作队列中,直到执行完毕。一个操作对象一次最多可以在一个操作队列中,如果操作已经在另一个操作队列中,该方法将抛出一个NSInvalidArgumentException 异常。类似地,如果操作正在执行或已经执行完毕,该方法将抛出NSInvalidArgumentException 异常。 |
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait |
将指定的操作添加到操作队列中;添加后,指定的操作将保留在队列中,直到完成的方法返回YES为止;指定的操作完成后,系统会将这些操作集从operations 数组中移除。参数ops 要添加到操作队列中的操作集;参数wait 如果YES ,则阻塞当前线程,直到所有指定的操作执行完毕;如果为NO ,则不会堵塞当前线程,立即返回。 |
- (void)addOperationWithBlock:(void (^)(void))block |
在操作中包装指定的块并将其添加到操作队列。参数block 是从操作中执行的块,该块不接受任何参数,也没有返回值。 |
- (void)waitUntilAllOperationsAreFinished |
阻塞当前线程,直到队列中所有操作执行完成为止。在此期间,当前线程不能向队列添加操作,但其他线程可以;完成所有挂起操作后,该方法返回;如果队列中没有操作,此方法立即返回;完成返回后,系统会将operations 的所有操作移除。 |
- (void)cancelAllOperations |
取消所有排队和执行的操作。此方法会对当前操作队列中的所有操作调用-(void)cancel 方法。取消操作不会自动将它们从队列中删除或停止当前正在执行的操作。对于排队和等待执行的操作,队列必须仍然尝试执行操作,然后才会识别它被取消并将其移动到完成状态。对于已经执行的操作,操作对象本身必须检查是否取消,并停止正在执行的操作,以便能够移动到完成状态。在这两种情况下,已完成(或已取消)的操作在从队列中删除之前仍然有机会执行其完成块。 |
属性 | 属性描述 |
---|---|
operations |
一个只读属性的数组,是当前操作队列中所有操作NSOperation 集合。该数组包含多个NSOperation 对象,其顺序与它们被添加到队列的顺序相同;这个顺序不一定反映执行这些操作的顺序。数组可能包含正在执行或等待执行的操作。列表还可能包含在数组最初被检索时正在执行的操作,但随后已经完成。 |
operationCount |
一个只读属性的NSUInteger 类型数据,表示操作队列中当前的并发数; |
2.1.3、管理与配置操作队列
属性 | 属性描述 |
---|---|
qualityOfService |
应用于添加到操作队列的操作对象的服务级别NSQualityOfService 。服务级别影响操作对象访问系统资源(如CPU时间、网络资源、磁盘资源等)的优先级;服务质量较高的操作优先于系统资源,因此可以更快地执行任务。对于自己创建的队列,默认值是NSOperationQualityOfServiceBackground 。对于主线程方法返回的队列,默认值是NSOperationQualityOfServiceUserInteractive ,无法更改。 |
maxConcurrentOperationCount |
操作队列可以同时执行的最大数量;设置并发操作的数量不会影响当前正在执行的任何操作。可以使用苹果推荐值NSOperationQueueDefaultMaxConcurrentOperationCount ,该值是根据当前系统条件动态确定的最大操作数。 |
name |
操作队列的名称,使用此名称方便调试或分析代码。此属性的默认值是一个包含操作队列内存地址的字符串。 |
underlyingQueue |
用于执行操作的分派队列dispatch_queue_t ,默认值为nil。可以将此属性的值设置为现有的调度队列,以便将排队操作与提交到该调度队列的块穿插在一起;只有在队列中没有操作时才应该设置此属性的值,当operationCount 不等于0时设置此属性的值会引发NSInvalidArgumentException 。该分派队列不能是主队列dispatch_get_main_queue 。该分派队列的服务质量级别集覆盖操作队列的服务质量;如果OS_OBJECT_IS_OBJC 是YES,此属性将自动保留其分配的队列。 |
2.1.4、操作队列暂停执行
属性suspended
:一个可读可写的布尔值,指示队列是否在主动调度要执行的操作。该属性的默认值为NO。当此属性的值为NO时,操作队列会启动队列中准备执行的操作;将此属性设置为YES可挂起操作队列中的任何排队操作,但已经执行的操作将继续执行。挂起的操作队列可以继续添加操作,但在将此属性更改为NO之前,这些操作不会计划执行。
属性suspended
在操作队列的作用类似于GCD中的dispatch_suspend()
函数,并不会立即暂停分派队列dispatch_queue
中正在执行的任务,而是在当前任务执行完成后,暂停后续的任务执行。
3、操作队列NSOperationQueue
与分派队列dispatch_queue_t
我们在开篇时提出:NSOperation与 NSOperationQueue 是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。那么苹果为什么要封装GCD
?使用NSOperationQueue
的优势是什么?
3.1、NSOperation
与dispatch_block_t
通过上文的学习,我们已经感受到NSOperation
很像GCD中的dispatch_block_t
。那么相比于dispatch_block_t
,使用Operation的优势如下:
- 可以给代码块添加
completionBlock
, 在任务完成以后自己调用. 相对于GCD代码更简洁;类似于GCD的dispatch_block_wait()
或者dispatch_block_notify()
-
NSOperation
之间可以添加依赖关系,- addDependency:
; - 设置
NSOperation
的优先级;类似dispatch_block_t
的qos - 方便的设置operation取消操作;类似
dispatch_block_cancel()
- 使用KVO观察对
NSOperation
状态的监听:isExcuting
,isFinished
,isCancelled
3.2、NSOperationQueue
与dispatch_queue_t
相比于分派队列dispatch_queue_t
,使用操作队列NSOperationQueue的优势如下:
- 对操作队列当前并发数的监控
operationCount
; - 对操作队列最大并发数的设置
maxConcurrentOperationCount
; - 可以调用
- cancelAllOperations
方法取消操作队列中尚未执行的操作;GCD中并没有提供取消分派队列dispatch_queue_t
中任务的函数; - 有时候我们很希望知道当前执行的操作队列是谁,我们可以使用
currentQueue
获取到当前的操作队列。但是GCD中分派队列dispatch_queue_t
是按照层级结构来组织的,无法单用某个队列对象来描述“当前队列”,dispatch_get_current_queue()
函数可能返回与预期不一致的结果,而且误用dispatch_get_current_queue()
可能导致死锁,所以GCD中dispatch_get_current_queue()
在 iOS 6已被废弃。