NSOperation

iOS2 时 OS X 与 iOS 的程序都普遍采用 NSOperation 来编写线程代码,而之后出现的 GCD 技术大体是依照前者的原则来实现的。随着 GCD 的普及,在iOS4 与 OS X 10.6 以后,OperationQueue 的底层都是用 GCD 来实现的。

1.与 GCD 的对比

1.1.GCD 的特点:

GCD 是面向过程的,它是由 C 语言构成的 API,一般与 block 结合使用,简洁高效;

1.2.NSOperation的特点:

面向对象: NSOperation 是面向对象的,拥有更多的函数可用;它能被继承,可以方便地重写其部分方法以实现特殊功能;也可以根据需求定义不同的子类,从而提高代码的复用率;

任务顺序可控制: NSOperation 能够设置依赖关系或任务优先级,方便的让处于同一个并行队列中的多个任务按照我们指定的顺序依次执行;GCD 中只能区分不同任务队列的优先级;

属性可监测: NSOperation 提供了部分属性,可通过 KVO 监听这些属性以确定任务是否完成或取消。这让我们能比 GCD 更加有效的掌任务进度和状态;

2.NSOperation

NSOperation 是个基类不能直接使,可以使用它的子类:NSInvocationOperation 和 NSBlockOperation。当然你也可以自定义一个基于 NSOperation 的子类。

2.1.子类

#示例1(NSInvocationOperation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)operationGo
{
//invocation Operation
NSInvocationOperation *invocOperation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:@selector(operationAction)
object:nil];

//执行结束后调用的Block
[invocOperation setCompletionBlock:^{
NSLog(@"++++invocation Finished");
}];
[invocOperation start];
}

- (void)operationAction
{
NSLog(@"++++execute invocation,
\n++++线程:%@",[NSThread currentThread]);
}

输出日志:

1
2
3
++++execute invocation,
++++线程:<NSThread: 0x600000065f80>{number = 1, name = main}
++++invocation Finished

#示例2(NSBlockOperation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)blockOperationGo
{
//block Operation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++execute block,\n++++线程:%@",[NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"++++added block, \n++++线程:%@",[NSThread currentThread]);
}];
[blockOperation setCompletionBlock:^{
NSLog(@"++++block Finished");
}];
[blockOperation start];
}

输出日志:

1
2
3
4
5
++++added block,
++++线程:<NSThread: 0x60400027c4c0>{number = 3, name = (null)}
++++execute block
++++线程:<NSThread: 0x604000078080>{number = 1, name = main}
++++block Finished

从上述示例及其输出结果中可以看到:

NSOperation 的俩子类在单独使用时都没有开辟新线程的能力,任务会在当前线程中同步地执行。

NSBlockOperation 使用addExecutionBlock添加任务时,这些任务可在新线程中并发地执行。

2.2.状态

1
2
3
4
BOOL executing;
BOOL finished;
BOOL asynchronous;
BOOL ready;
  • isReady

表示这个 operation 是否已做好准备去执行。=NO 表示它依赖的相关 operation 尚未完成。

  • isExecuting

表示当前 operation 中的任务是否正在执行。

  • isFinished

= YES 时表示当任务已完成或者已被取消。只有此属性为YES时 operation 才会从队列中出列。

  • isCancelled

当前 operation 是否被取消了。在自定义子类的任务开始前,需要在 start 方法中检查此属性。当 = YES 时应该立刻退出,同时将 finished属性设置为YES,executing 属性设置为 NO。

  • isAsynchronous

表示 operation 在当前线程是异步还是同步执行,默认值为NO。执行异步 operation 时必须重写此属性并返回 YES。

注意:NSOperationQueue 是通过 KVO 观察 NSOperation 的状态变化,来自动管理 NSOperation 的执行。重写 start 方法时,我们必须自己维护 isExecuting、isFinished 的值并正确的发送相关 KVO 通知。

2.3.优先级

NSOperation的优先级可以由其属性queuePriority指定:

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

注意:与GCD中队列的优先级不一样,这个属性是队列中 Operation 即任务的优先级:

The execution priority of the operation in an operation queue.

从这点来说,Operation 比GCD的功能更丰富,因为GCD只能给队列设置优先级,队列中的任务之间没法单独设置优先级。并且GCD只能给GlobalQueue设置优先级,其他队列想要设置优先级,需要通过下面这种方式进行:

1
2
3
4
5
6
dispatch_queue_t serialQueue = dispatch_queue_create("com.xx.xx",DISPATCH_QUEUE_SERIAL);

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);

//第一个参数是目标队列,第二个参数是参考队列(全局队列);
dispatch_set_target_queue(serialQueue, globalQueue);

对于此queuePriority有三点需要说明:

  • 优先级决定了各 operation 之间的开始顺序;
  • 优先级只针对同一队列中的 operation,不同队列中的任务之间比较优先级无意义;
  • 优先级只针对并发队列,对于并发数为 1 的串行队列来说无意义。

理论上来说优先级高的先执行,但实际测试中发现,通过这个属性标记的任务,其开始的顺序并不一定按照我们指定的优先级来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)operationPriority{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++3");
}];

op1.queuePriority = NSOperationQueuePriorityNormal;
op2.queuePriority = NSOperationQueuePriorityVeryHigh;
op3.queuePriority = NSOperationQueuePriorityVeryLow;

NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperations:@[op1,op2,op3] waitUntilFinished:NO];
}

日志:

1
2
3
+++3
+++1
+++2

三个任务实际执行时并未按照我们指定的2->1->3的顺序来。

决定任务开始顺序的不仅是优先级,还要看任务是否处在isReady状态:

The readiness of operations is determined by their dependencies on other operations and potentially by custom conditions that you define. The NSOperation class manages dependencies on other operations and reports the readiness of the receiver based on those dependencies.

By default, an operation object that has dependencies is not considered ready until all of its dependent operation objects have finished executing. Once the last dependent operation finishes, however, the operation object becomes ready and able to execute.

官文中说ready是根据当前任务“是否有依赖任务及该任务是否已完成”而定的。示例中的三个任务之间是并发且没有依赖关系的,所以按理它们被加入队列后,是否处在就绪状态是由队列或线程池状况来决定的,至于最终设置了优先级但失效了的问题,这不是我们能控制的了。

鉴于此,为了更精确控制任务优先级,还是推荐使用接下来介绍的qualityOfService服务质量属性来指定任务优先级。这里只是说更精确的控制,不是说服务质量能完全保证任务按指定顺序执行,下面会具体介绍。

2.4.服务质量

qualityOfService,这是在iOS 8后推出的新属性,通过设置服务质量来决定任务在队列中的优先级,NSOperation和NSOperationQueue都有这个属性。

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
};
  • UserInteractive

Used for work directly involved in providing an interactive UI. For example, processing control events or drawing to the screen.

用于涉及到UI交互的场景,例如处理点击事件或绘制图片到屏幕上。

  • UserInitiated

Used for performing work that has been explicitly requested by the user, and for which results must be immediately presented in order to allow for further user interaction. For example, loading an email after a user has selected it in a message list.

用于用户触发的、需要立刻返回结果的任务,例如用户点击邮件列表后立刻加载邮件内容。

  • Default

Indicates no explicit quality of service information. Whenever possible, an appropriate quality of service is determined from available sources. Otherwise, some quality of service level between NSQualityOfServiceUserInteractive and NSQualityOfServiceUtility is used.

默认值,表示未指明服务质量,系统会根据当前可用资源的状况,使用介于UserInteractiveUtility之间的某个优先级。

  • Utility

Used for performing work which the user is unlikely to be immediately waiting for the results. This work may have been requested by the user or initiated automatically, and often operates at user-visible timescales using a non-modal progress indicator. For example, periodic content updates or bulk file operations, such as media import.

用于不需要立刻返回结果的任务,比如周期性的更新内容,或者导入媒体文件。

  • Background

Used for work that is not user initiated or visible. In general, a user is unaware that this work is even happening. For example, pre-fetching content, search indexing, backups, or syncing of data with external systems.

后台任务,优先级最低,用于非用户触发的、不可见的任务,比如发起的网络请求、检索结果、同步数据。

一般来说,服务质量对应的优先级排序是:UserInteractive > UserInitiated > Default > Utility > Background。

#示例:服务质量决定优先级

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
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self operationServiceQuality];
}

- (void)operationServiceQuality{
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++3");
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++4");
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"+++5");
}];

op1.qualityOfService = NSQualityOfServiceUserInitiated;
op2.qualityOfService = NSQualityOfServiceDefault;
op3.qualityOfService = NSQualityOfServiceUserInteractive;
op4.qualityOfService = NSQualityOfServiceUtility;
op5.qualityOfService = NSQualityOfServiceBackground;

NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperations:@[op1,op2,op3,op4,op5] waitUntilFinished:NO];

}

日志:

1
2
3
4
5
+++3
+++1
+++2
+++4
+++5

但是,多次运行同样一段代码之后,会得到这样的日志:

1
2
3
4
5
+++1
+++3
+++2
+++4
+++5

和这样的日志:

1
2
3
4
5
+++3
+++1
+++5
+++2
+++4

结论:服务质量也不能保证任务的执行顺序。

那咋办呢?为了精确控制任务的顺序,还是建议使用依赖关系,或者将任务放到最大并发数=1的串行队列中。

3.NSOperationQueue

NSOperation 单独使用时不具备开启新线程的能力,只有配合 NSOperationQueue 才能开辟新线程执行任务。NSOperation 相当于GCD中block内的任务,而 NSOperationQueue 则相当于GCD中的队列。将任务加入到队列中后,系统会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作,不需要再手动调用start。

3.1.两种队列

  • 主队列:(任务会在主线程执行)
1
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
  • 其他队列:(任务会放到子线程中执行)。
1
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

3.2.添加任务

#示例3:-addOperation:(NSOperation *)op;

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
- (void)addOperation
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSInvocationOperation *op1 = [[NSInvocationOperation alloc]
initWithTarget:self
selector:@selector(run)
object:nil];

NSBlockOperation *op2 = [NSBlockOperation
blockOperationWithBlock:^{
NSLog(@"++++执行block任务,
\n++++线程:%@",
[NSThread currentThread]);
}];

[queue addOperation:op1];
[queue addOperation:op2];
}

- (void)run
{
NSLog(@"++++执行invocation operation任务,
\n++++线程:%@", [NSThread currentThread]);
}

主线程调用之后,输出日志:

1
2
3
4
++++执行block任务,
++++线程:<NSThread: 0x604000268cc0>{number = 3, name = (null)}
++++执行invocation operation任务,
++++线程:<NSThread: 0x604000268c80>{number = 4, name = (null)}

#示例4:-addOperationWithBlock:(void (^)(void))block;

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
- (void)addBlockOperation
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//最大并发数
//queue.maxConcurrentOperationCount = 1;
//queue.maxConcurrentOperationCount = 2;

[queue addOperationWithBlock:^{
NSLog(@"++++执行block任务1,
\n++++线程:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
NSLog(@"++++执行block任务2,
\n++++线程:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
NSLog(@"++++执行block任务3,
\n++++线程:%@", [NSThread currentThread]);
}];

[queue addOperationWithBlock:^{
NSLog(@"++++执行block任务4,
\n++++线程:%@", [NSThread currentThread]);
}];
}

主线程调用之后,输出日志:

1
2
3
4
5
6
7
8
++++执行block任务3
++++线程:<NSThread: 0x60000046c180>{number = 3, name = (null)}
++++执行block任务4
++++线程:<NSThread: 0x60000046c1c0>{number = 5, name = (null)}
++++执行block任务1
++++线程:<NSThread: 0x60400026a000>{number = 4, name = (null)}
++++执行block任务2
++++线程:<NSThread: 0x60400026a280>{number = 6, name = (null)}

与 NSOperationQueue 结合后,任务都在不同子线程里并发执行,具备了开启新线程的能力。

3.3.并发数

  • -1,默认值,表示不进行限制,默认为并发执行;
  • =0,阻塞、都不执行;
  • =1,串行;
  • >1,并发;

上面#示例4中,如果让 maxConcurrentOperationCount = 1,则队列为串行,输出日志如下:

1
2
3
4
5
6
7
8
++++执行block任务1
++++线程:<NSThread: 0x604000272240>{number = 3, name = (null)}
++++执行block任务2
++++线程:<NSThread: 0x604000272240>{number = 3, name = (null)}
++++执行block任务3
++++线程:<NSThread: 0x604000272240>{number = 3, name = (null)}
++++执行block任务4
++++线程:<NSThread: 0x604000272240>{number = 3, name = (null)}

可以看出,队列中的任务按照串行的方式在执行。不设置或者=2时,任务会并发执行,开启线程数量是由系统决定的,不需要我们来管理。

3.4.依赖关系

#示例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)dependenceGo
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op3");
}];

[op1 addDependency:op2];
[op2 addDependency:op3];

[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}

输出日志如下:

1
2
3
++++执行op3
++++执行op2
++++执行op1

不添加依赖时,3个任务会并发执行,先后顺序不定。添加依赖后,3个任务按照设定倒序执行。

3.5.取消任务

  • 取消单个任务
1
- (void)cancel;//NSOperation的方法 取消单个任务。

作用:建议operation对象停止执行它包含着的任务。

Advises the operation object that it should stop executing its task.

此方法并不会强制任务对象停止执行,它只是更新对象中的标记位来反映任务状态的变化。

  • 如果任务已经完成,则取消对它没有影响;
  • 如果任务在某个队列中但尚未开始执行,则执行取消操作时任务会从队列中移除;
  • 如果任务尚未加入队列中,则执行取消操作会让任务立刻被标记为finished;

This method does not force your operation code to stop. Instead, it updates the object’s internal flags to reflect the change in state. If the operation has already finished executing, this method has no effect. Canceling an operation that is currently in an operation queue, but not yet executing, makes it possible to remove the operation from the queue sooner than usual.
In macOS 10.6 and later, if an operation is in a queue but waiting on unfinished dependent operations, those operations are subsequently ignored. Because it is already cancelled, this behavior allows the operation queue to call the operation’s start method sooner and clear the object out of the queue. If you cancel an operation that is not in a queue, this method immediately marks the object as finished. In each case, marking the object as ready or finished results in the generation of the appropriate KVO notifications.
In versions of macOS prior to 10.6, an operation object remains in the queue until all of its dependencies are removed through the normal processes. Thus, the operation must wait until all of its dependent operations finish executing or are themselves cancelled and have their start method called.
For more information on what you must do in your operation objects to support cancellation, see Responding to the Cancel Command.

在macOS10.6之前此取消方法并不会立刻将当前operation移出队列,而是要待到它所依赖的其他operation全部执行完,或者都被取消并且执行完它们的start方法后。而在macOS10.6之后,取消一个operation时会自动忽略它所依赖的其他operation,并且队列会提前调用当前operation的start方法,随后将其移出队列。注意:这句话隐含的意思是,即使operation被取消了,其start方法仍会被调用。所以我们在重写operation的start方法时,要注意检测当前任务的isCancelled属性,如果任务已经被取消则应立刻返回,不再继续后续操作。

  • 取消所有任务
1
- (void)cancelAllOperations;//NSOperationQueue的方法,取消队列中的所有任务。

作用:取消队列中所有的operation,包括尚未执行和已执行的operation。

This method calls the cancel method on all operations currently in the queue.
Canceling the operations does not automatically remove them from the queue or stop those that are currently executing. For operations that are queued and waiting execution, the queue must still attempt to execute the operation before recognizing that it is canceled and moving it to the finished state. For operations that are already executing, the operation object itself must check for cancellation and stop what it is doing so that it can move to the finished state. In both cases, a finished (or canceled) operation is still given a chance to execute its completion block before it is removed from the queue.

此方法会调用队列中所有operation的cancel方法,但不会停止正在执行中的operation,也不会立刻将这些任务移出队列。对尚未执行的任务,队列仍然会去尝试执行它们,即前面提到的继续执行start方法,直到检测到任务被取消并且其状态为finished。对于正在执行中的任务,其内部必须检查isCancelled属性是否为YES,如果是则需要立刻停止后续操作。这两种情况下,被取消或状态为finished的operation没有被立刻停止和移出队列,从而让它们还有机会执行自己的完成回调。

3.6.暂停/恢复

1
- (void)setSuspended:(BOOL)b; // NSOperationQueue中的方法

作用:暂停或恢复当前队列中所有的任务。

When the value of this property is NO, the queue actively starts operations that are in the queue and ready to execute. Setting this property to YES prevents the queue from starting any queued operations, but already executing operations continue to execute. You may continue to add operations to a queue that is suspended but those operations are not scheduled for execution until you change this property to NO.
Operations are removed from the queue only when they finish executing. However, in order to finish executing, an operation must first be started. Because a suspended queue does not start any new operations, it does not remove any operations (including cancelled operations) that are currently queued and not executing.
You may monitor changes to the value of this property using Key-value observing. Configure an observer to monitor the suspended key path of the operation queue.
The default value of this property is NO.

该字段是队列的属性:

  • 值为NO时,队列会启动队已准备就绪的任务;
  • 值为YES时,会阻止启动队列中的任务;
  • 已经在执行的任务会继续执行;
  • 往已经暂停的队列中添新加任务时,这些任务也会被暂停,直到暂停状态被置为NO;

Operation只有在finished状态时才会从队列中出列,想让任务finished,须先启动此任务;而暂停状态的队列并不会启动任何新的任务,所以队列也就不会移除任何任务,包括已经被cancel的任务。

对应场景:一个已经被暂停的队列中有N个任务,其中任务A还在排队中尚未开始执行,此时对任务A执行cancel方法,则任务A不会出列。因为此时队列在暂停状态,任务A又没开始执行,所以其start方法也就不会被调用,isFinished状态也就还不是YES,因此也就不会出列~

小结:对于上面三个方法,可以这么简单的理解:Operation必须跑起来才能更新和检测自己的isCanceled及isFinished状态,从而决定自己是不是该取消和出列。

4.实现Group任务

GCD中提供了dispatch_group以便将多个任务组合在一起,同时可以在这些任务执行完成后通过dispatch_group_notify的回调执行某些任务。NSOperation并没有Group相关字眼的接口,但是我们仍然可以在NSOperation已有接口的基础上变相实现此功能。

4.1.依赖关系

思路:给封装了最后任务的 Operation 设置依赖关系,使其依赖于前面的所有任务。

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
- (void)monitorGroupTasks
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op2");
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op3");
}];

//最后需要执行的UI任务
NSBlockOperation *finOp = [NSBlockOperation blockOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"++++最后执行UI任务~");
}];
}];

//添加依赖,让finOp依赖于Grop中的所有任务
[finOp addDependency:op1];
[finOp addDependency:op2];
[finOp addDependency:op3];

[queue addOperations:@[finOp,op3,op2,op1] waitUntilFinished:NO];
}

日志:

1
2
3
4
++++执行op2
++++执行op1
++++执行op3
++++最后执行UI任务~

4.2.completionBlock

思路:NSBlockOperation中的addExecutionBlock接口可以给operation添加额外任务并在异步线程中执行。同时NSOperation提供了completionBlock属性,可以利用它来在所有任务执行完成后再执行某个特殊任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)monitorGroupTasks
{
NSBlockOperation *blockOpration = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"++++执行op1,线程:%@",[NSThread currentThread]);
}];
[blockOpration addExecutionBlock:^{
NSLog(@"++++执行op2,线程:%@",[NSThread currentThread]);
}];
[blockOpration addExecutionBlock:^{
NSLog(@"++++执行op3,线程:%@",[NSThread currentThread]);
}];

//最后需要执行的UI任务
[blockOpration setCompletionBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"++++最后执行UI任务~,线程:%@",[NSThread currentThread]);
}];
}];

//开启任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOpration];
}

日志:

1
2
3
4
++++执行op1,线程:<NSThread: 0x600003594d40>{number = 3, name = (null)}
++++执行op3,线程:<NSThread: 0x6000035d9c80>{number = 5, name = (null)}
++++执行op2,线程:<NSThread: 0x6000035eeb40>{number = 4, name = (null)}
++++最后执行UI任务~,线程:<NSThread: 0x6000035f5dc0>{number = 1, name = main}

5.自定义NSOperation

自定义时,需要重写 NSOperation 的startmain方法。

5.1.重写main

1
- (void)main;

Performs the receiver’s non-concurrent task,执行非并发任务时重写此函数。

NSOperation 默认是非并发的,在不加入 Queue 的情况下,通过调用 -(void)start 对象方法在当前线程执行任务时, 会一直阻塞当前线程,直到任务完成。-(void)main方法默认情况下不做任何事,执行完成后属性isExecuting会被置为 NO, 属性isFinished被置为 YES。我们可以重写此方法以实现自定义的任务。重写时不能调用super,也不需要手动创建 autorelease pool,因为系统已自动帮你实现。

#示例6:自定义非并发 NSOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)main
{
if (self.isCancelled) return;
NSData *imageData = [[NSData alloc] initWithContentsOfURL:
[NSURL URLWithString:_mUrlStr]];
if (self.isCancelled) {
imageData = nil;
return;
}
if (self.isCancelled) return;
if ([_delegate respondsToSelector:@selector(downloadFinishedWithData:)]) {
[_delegate downloadFinishedWithData:imageData];
}
}

5.2.重写start

1
- (void)start;

Begins the execution of the operation,If you are implementing a concurrent operation, you must override this method and use it to initiate your operation。

执行并发任务时“必须”重写此方法。

在没有被重写时,start方法的默认实现里会更新当前任务的执行状态,之后调用-main函数。此方法也会做一些检查,以确保当前任务能正常执行:如果任务已被取消或已完成,则-start方法直接返回并且不会再调用-main方法;如果任务正在执行或者尚未准备就绪,则会抛出异常;

An operation is not considered ready to execute if it is still dependent on other operations that have not yet finished.

重写start方法时不能调用super函数,需要及时更新 isExecuting 和 isFinished 属性,并发出对应的 KVO 通知,因为在并发情况下系统不知道任务什么时候完成。自定义时,我们一般会将任务定义为异步执行,也就是说start函数返回了任务不一定就是完成了。 这个要你自己来控制,只有将 isFinished 置为 YES 时,operation 才算完成,才能出列和销毁。

5.3.自定义示例

#示例7:自定义NSOperation子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//NetOperation.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@class NetOperation;
//代理协议
@protocol NetOperationDelegate<NSObject>
- (void)downloadFinishedWithData:(id)data;
@end

@interface NetOperation : NSOperation
- (instancetype)initWithUrl:(NSString *)url
delegate:(id<NetOperationDelegate>)delegate;
@end
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//NetOperation.m
#import "NetOperation.h"

@interface NetOperation()

@property (nonatomic, copy) NSString *mUrlStr;
@property (nonatomic, weak) id<NetOperationDelegate>delegate;
@property (nonatomic, strong) NSURLSessionDownloadTask *dataTask;

@end

@implementation NetOperation
//因为父类的属性是readonly的,重载时如果需要setter的话则需要手动合成。
@synthesize finished = _finished, executing = _executing;

// 这里需要实现KVO相关的方法,NSOperationQueue是通过KVO来判断任务状态的

- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isAsynchronous{
return YES;
}

- (instancetype)initWithUrl:(NSString *)url
delegate:(id<NetOperationDelegate>)delegate{
if (self = [super init]) {
_delegate = delegate;
_mUrlStr = url;
}
return self;
}

- (void)dealloc{
_delegate = nil;
}

#pragma mark -重载start方法 实现业务需求
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
}

- (void)cancel {
@synchronized (self) {
[super cancel];
// 如果正在执行中则表示已经start过,可以将isFinished设为yes
if (self.isExecuting) {
self.finished = YES;
self.executing = NO;
}
[_dataTask cancel];
}
}

#pragma mark -BUSINESS
-(void)main{
NSURLSession *session = [NSURLSession sharedSession];
__weak typeof(self) weakSelf = self;
_dataTask = [session downloadTaskWithURL:[NSURL URLWithString:_mUrlStr]
completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) {
if (!error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSData *data = [NSData dataWithContentsOfFile:location.path];
strongSelf.finished = YES;
strongSelf.executing = NO;
if ([strongSelf.delegate respondsToSelector:@selector(downloadFinishedWithData:)]) {
[strongSelf.delegate downloadFinishedWithData:data];
}
}
}];
//开始任务
[_dataTask resume];
}
@end

导入头文件,创建并执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(BOOL)application:(UIApplication *)application 
willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
NetOperation *anOperation = [[NetOperation alloc] initWithUrl:@"xxx" delegate:self];
[aQueue addOperation:anOperation];

return YES;
}

- (void)downloadFinishedWithData:(id)data
{
UIImage *image = [UIImage imageWithData:data];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"++++++Reqest Finished!");
}];
}

本示例只是最简单的自定义,更复杂的可以参考AFN和SD中Operation相关的类~


相关参考:

#©Apple-Operation Queues


NSOperation
https://davidlii.cn/2017/11/17/nsoperation.html
作者
Davidli
发布于
2017年11月17日
许可协议