NSOperation 與 NSOperationQueue
NSOperation和NSOperationQueue是Apple基于GCD封裝的一套面向?qū)ο蟮腁PI.
使用Operation的優(yōu)勢如下:
- 可以給代碼塊添加completionBlock, 在任務(wù)完成以后自己調(diào)用. 相對于GCD代碼更簡潔.(類似于GCD的
dispatch_block_wait/dispatch_block_notify) - operation之間可以添加依賴關(guān)系. (addDependency)
- 設(shè)置operation的優(yōu)先級.(類似與gcd block的qos)
- 方便的設(shè)置operation取消操作(gcd的
dispatch_block_cancel) - 使用KVO觀察對operation狀態(tài)的監(jiān)聽: isExcuting, isFinished, isCancelled.
NSOperation就是操作, 類似GCD中的block.通常有NSInvocationOperation、NSBlockOperation, 以及自定義NSOperation三種.
NSOperationQueue 是操作隊列, 即存放operation的隊列. NSOperationQueue將Operation添加到隊列中以后, Operation首先進(jìn)入ready狀態(tài)(是否ready取決與不同operation之間的依賴dependency), 如果ready狀態(tài)的operation會開始按照operation的優(yōu)先級, 順序被調(diào)用執(zhí)行.操作隊列通過maxConcurrentOperationCount屬性, 控制并發(fā),串行.(類似于GCD的并行和串行隊列).
NSOperation的生命周期

如上圖所示具有四個階段:
- Pending
- Ready
- Executing
- Finished
當(dāng)我們初始化創(chuàng)建一個NSOperation實例的時候, NSOperation的狀態(tài)是Pending狀態(tài).
如果該operation沒有依賴, 被添加到NSOperationQueue的時, 狀態(tài)會依次 Ready -> Executing -> Finished.
如果有依賴, 被添加到NSOperationQueue的時, 狀態(tài)會是Pending. 等待前序Operation執(zhí)行, 狀態(tài)成為Finished以后, 本Operation狀態(tài)才會轉(zhuǎn)化成Ready, 然后 Ready -> Executing -> Finished.

除此之外, 在Pending, Ready, Executing狀態(tài)都可以調(diào)用cancel, 讓Operation進(jìn)入 cancelled狀態(tài).
注意, cancelled 狀態(tài)和 finished狀態(tài)有race condition.由于cancel只會將
cancelled屬性設(shè)置為YES, 在實際情況中, 有些情況是無法立刻取消的. 例如有一個取消按鈕, 點擊以后取消需要一定的時間,在這段時間內(nèi)容, Operation結(jié)果完成Finished, 如何處理這種情況? 因此Finished狀態(tài)與isCancelled狀態(tài)的的臨界區(qū)域需要我們?nèi)ビ枚嗑€程方法去保護(hù), 避免多線程競爭導(dǎo)致意想不到的結(jié)果.
NSOperation多種類型
NSOperation的種類有三種:
- NSInvocationOperation
- NSBlockOperation
- 自定義NSOperation
NSOperation有三種狀態(tài),isReady, isExecuting, isFinished.還有很多其他屬性, 隨著執(zhí)行操作變化而變化.
NSInvocationOperation調(diào)用start執(zhí)行
調(diào)用實例:
/**
1. start---<NSThread: 0x604000071440>{number = 1, name = main}
2. 1---<NSThread: 0x604000071440>{number = 1, name = main}
3. 2---<NSThread: 0x604000071440>{number = 1, name = main}
4. end--- <NSThread: 0x604000071440>{number = 1, name = main}
*/
- (void) operationStartDemo2 {
NSLog(@"start---%@", [NSThread currentThread]);
// 1.創(chuàng)建 NSInvocationOperation 對象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 2.調(diào)用 start 方法開始執(zhí)行操作
[op start];
NSLog(@"end--- %@", [NSThread currentThread]);
}
- (void)task2 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"%d---%@", i,[NSThread currentThread]); // 打印當(dāng)前線程
}
}
結(jié)果解釋:
這種創(chuàng)建Operation(NSBlockOperation/NSInvocationOperation), 然后直接調(diào)用start(), 會在當(dāng)前線程中執(zhí)行task.(類似于創(chuàng)建一個block,然后直接調(diào)用block()).
NSBlockOperation
/**
1. start---<NSThread: 0x600000070d00>{number = 1, name = main}
2. 0---<NSThread: 0x600000070d00>{number = 1, name = main}
3. 1---<NSThread: 0x600000070d00>{number = 1, name = main}
4. end--- <NSThread: 0x600000070d00>{number = 1, name = main}
*/
- (void)operationStartDemo3 {
NSLog(@"start---%@", [NSThread currentThread]);
// 1.創(chuàng)建blockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"%d---%@", i,[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
// 2.調(diào)用 start 方法開始執(zhí)行操作
[op start];
NSLog(@"end--- %@", [NSThread currentThread]);
}
如果NSBlockOperation中只有一個任務(wù), 那么調(diào)用start在當(dāng)前線程中同步執(zhí)行.
/**
1. start---<NSThread: 0x60400006c480>{number = 1, name = main}
2. 4---<NSThread: 0x60400027b500>{number = 4, name = (null)}
3. 2---<NSThread: 0x60400006c480>{number = 1, name = main}
4. 1---<NSThread: 0x60400027b5c0>{number = 5, name = (null)}
5. 3---<NSThread: 0x60000026d280>{number = 3, name = (null)}
6. 1---<NSThread: 0x60400027b5c0>{number = 5, name = (null)}
7. 2---<NSThread: 0x60400006c480>{number = 1, name = main}
8. 4---<NSThread: 0x60400027b500>{number = 4, name = (null)}
9. 3---<NSThread: 0x60000026d280>{number = 3, name = (null)}
10 end--- <NSThread: 0x60400006c480>{number = 1, name = main}
*/
- (void)blockOperationAddOperationDemo {
NSLog(@"start---%@", [NSThread currentThread]);
// 1.創(chuàng)建blockOperation
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
// 2.添加額外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印當(dāng)前線程
}
}];
// 2.調(diào)用 start 方法開始執(zhí)行操作
[op start];
NSLog(@"end--- %@", [NSThread currentThread]);
}
在NSBlockOperation對象上,調(diào)用addExecutionBlock添加了多個task, 結(jié)果看出, operation都是在子線程中運行的, 系統(tǒng)會開啟多個子線程去并行運行加入的blocks.
再看下addExecutionBlock:的注釋:
Adds the specified block to the receiver’s list of blocks to perform.
The specified block should not make any assumptions about its execution environment.
Calling this method while the receiver is executing or has already finished causes an NSInvalidArgumentException exception to be thrown.
如果NSBlockOperation的狀態(tài)是excuting,finished, 調(diào)用該方法會拋出異常.
總結(jié):
一般情況下,如果一個 NSBlockOperation 對象封裝了多個操作。NSBlockOperation 是否開啟新線程,取決于操作的個數(shù)。如果添加的操作的個數(shù)多,就會自動開啟新線程。當(dāng)然開啟的線程數(shù)是由系統(tǒng)來決定的。
NSOperation是一錘子買賣, 也就是說NSOperation在狀態(tài)變化以后, 不能返回到原來的狀態(tài).
自定義NSOperation
調(diào)用方法同前面的一樣, 直接調(diào)用start方法或者加入到operationQueue中.
NSOperation的結(jié)束監(jiān)聽
NSOperation可以設(shè)置一個 comletionBlock, 在NSOperation執(zhí)行完成的時候執(zhí)行.
/**
1. start---<NSThread: 0x604000065d00>{number = 1, name = main}
2. end---<NSThread: 0x604000065d00>{number = 1, name = main}
3. 1---<NSThread: 0x600000072c40>{number = 3, name = (null)}
4. 1---<NSThread: 0x600000072c40>{number = 3, name = (null)}
5. operation end --- <NSThread: 0x6040002750c0>{number = 4, name = (null)}
*/
-(void)NSOperationCompletion1{
NSLog(@"start---%@", [NSThread currentThread]);
NSOperationQueue *oq = [NSOperationQueue new];
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op setCompletionBlock:^{
NSLog(@"operation end --- %@",[NSThread currentThread]); // 打印當(dāng)前線程
}];
[oq addOperation:op];
NSLog(@"end---%@", [NSThread currentThread]);
}
/**
1. start---<NSThread: 0x604000071e40>{number = 1, name = main}
2. 1---<NSThread: 0x604000071e40>{number = 1, name = main}
3. 1---<NSThread: 0x604000071e40>{number = 1, name = main}
4. end---<NSThread: 0x604000071e40>{number = 1, name = main}
5. operation end --- <NSThread: 0x60000046c480>{number = 3, name = (null)}
*/
-(void)NSOperationCompletion2{
NSLog(@"start---%@", [NSThread currentThread]);
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op setCompletionBlock:^{
NSLog(@"operation end --- %@",[NSThread currentThread]); // 打印當(dāng)前線程
}];
[op start];
NSLog(@"end---%@", [NSThread currentThread]);
}
通過Demo, 我們可以看出completionBlock執(zhí)行的內(nèi)容都是在子線程中執(zhí)行的(不論是使用start直接調(diào)用, 還是添加到OperationQueue).
NSOperation的completionBlock執(zhí)行是在 NSOperation的 finished 屬性被設(shè)置以后. 實際中finished可能是Operation被取消cancelled, 或者真正的執(zhí)行完成completed, 這兩種情況需要我們自主區(qū)分, 尤其在自定義NSOperation的時候.
不論是直接start調(diào)用還是添加到OperationQueue中,不論queue是并行隊列還是串行隊列. NSOperation的completionBlock總是在子線程中執(zhí)行.
實際NSOperation的
finished屬性被KVO監(jiān)聽, 如果一旦finished, 就執(zhí)行completionBlock.
NSOperationQueue
- NSOperation 可以調(diào)用 start 方法來執(zhí)行任務(wù),但默認(rèn)是同步執(zhí)行的
- 如果將 NSOperation 添加到 NSOperationQueue(操作隊列)中,系統(tǒng)會自動異步執(zhí)行NSOperation中的操作
有兩種方式將Operation添加到NSOperationQueue中:
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
下面是通過多重方式將各種NSOperation添加到NSOperationQueue的demo:
/**
1. start---<NSThread: 0x600000262840>{number = 1, name = main}
2. end---<NSThread: 0x600000262840>{number = 1, name = main}
3. 1---<NSThread: 0x600000661ec0>{number = 3, name = (null)}
4. 4---<NSThread: 0x604000267800>{number = 5, name = (null)}
5. 3---<NSThread: 0x6000006620c0>{number = 4, name = (null)}
6. 2---<NSThread: 0x604000267b80>{number = 6, name = (null)}
7. 4---<NSThread: 0x604000267800>{number = 5, name = (null)}
8. 1---<NSThread: 0x600000661ec0>{number = 3, name = (null)}
9. 3---<NSThread: 0x6000006620c0>{number = 4, name = (null)}
10 2---<NSThread: 0x604000267b80>{number = 6, name = (null)}
*/
-(void)operationQueueDemo1{
NSOperationQueue *oq = [[NSOperationQueue alloc]init];
NSLog(@"start---%@", [NSThread currentThread]);
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
PPOperation *op3 = [PPOperation new];
[oq addOperation:op1];
[oq addOperation:op2];
[oq addOperation:op3];
[oq addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSLog(@"end---%@", [NSThread currentThread]);
}
/**
1. start---<NSThread: 0x600000074c00>{number = 1, name = main}
2. end---<NSThread: 0x600000074c00>{number = 1, name = main}
3. 4---<NSThread: 0x600000464880>{number = 3, name = (null)}
4. 4---<NSThread: 0x600000464880>{number = 3, name = (null)}
*/
-(void)operationQueueDemo2{
NSOperationQueue *oq = [[NSOperationQueue alloc]init];
NSLog(@"start---%@", [NSThread currentThread]);
[oq addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSLog(@"end---%@", [NSThread currentThread]);
}
結(jié)果OperationQueue會異步開啟新線程執(zhí)行添加的Operation.
NSOperationQueue設(shè)置成并發(fā)隊列與串行隊列
可以通過設(shè)置NSOperationQueue的maxConcurrentOperationCount屬性,控制該Queue是串行執(zhí)行、并發(fā)執(zhí)行.
默認(rèn)情況下NSOperationQueue是并發(fā)隊列, 系統(tǒng)控制最大并發(fā)數(shù).如果設(shè)置maxConcurrentOperationCount = 1, 那么就是串行隊列, 下面是實例.
/**
1. start---<NSThread: 0x600000071a00>{number = 1, name = main}
2. end---<NSThread: 0x600000071a00>{number = 1, name = main}
3. 1---<NSThread: 0x60000046d6c0>{number = 3, name = (null)}
4. 1---<NSThread: 0x60000046d6c0>{number = 3, name = (null)}
5. 2---<NSThread: 0x60000027ea40>{number = 4, name = (null)}
6. 2---<NSThread: 0x60000027ea40>{number = 4, name = (null)}
*/
-(void)operationQueueDemo3{
NSOperationQueue *oq = [[NSOperationQueue alloc]init];
NSLog(@"start---%@", [NSThread currentThread]);
// 設(shè)置Queue是串行隊列
[oq setMaxConcurrentOperationCount:1];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[oq addOperation:op1];
[oq addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSLog(@"end---%@", [NSThread currentThread]);
}
結(jié)論: 串行隊列 + 異步調(diào)用.
NSOperationQueue的暫停與恢復(fù)
隊列的暫停是 當(dāng)前正在執(zhí)行的任務(wù)完成以后, 暫停后面執(zhí)行的任務(wù), 相關(guān)API:
//暫停和恢復(fù)隊列(YES代表暫停隊列,NO代表恢復(fù)隊列)
- (void)setSuspended:(BOOL)b;
//當(dāng)前狀態(tài)
- (BOOL)isSuspended;
暫停和恢復(fù)的使用場合:
在tableview界面,開線程下載遠(yuǎn)程的網(wǎng)絡(luò)界面,對UI會有影響,使用戶體驗變差。那么這種情況,就可以設(shè)置在用戶操作UI(如滾動屏幕)的時候,暫停隊列(不是取消隊列),停止?jié)L動的時候,恢復(fù)隊列。
NSOperation的屬性與操作設(shè)置
理解Operation的就緒狀態(tài):
舉個例子,現(xiàn)在有2個操作:op1,op2。其中 op2 依賴于 op1, 即 op2 -> op1。現(xiàn)在將這2個操作添加到隊列中并發(fā)執(zhí)行。
- 因為 op1 沒有要依賴的任務(wù),所以op1執(zhí)行之前,就是處于ready狀態(tài)的操作。
- 而op2 是有依賴的operation,所以op2當(dāng)前狀態(tài)就是非ready狀態(tài).
只有進(jìn)入就緒狀態(tài)的ready才能被添加到queue中調(diào)度執(zhí)行,或者直接調(diào)用start執(zhí)行.如果是非ready狀態(tài)的operation, 直接調(diào)用start方法會crash.
Operation依賴相關(guān)API
不同的NSOperation之間可以添加依賴關(guān)系, 方便我們控制操作之間的執(zhí)行順序. 相關(guān)接口如下:
- (void)addDependency:(NSOperation *)op:- (void)removeDependency:(NSOperation *)op;@property (readonly, copy) NSArray *dependencies;
[op2 addDependency:op1];這里是讓op2 依賴于 op1,則先執(zhí)行op1,然后執(zhí)行op2.
/**
2018-07-05 17:25:54.129570+0800 NSOperationDemo[22605:1156299] start---<NSThread: 0x60400007dc80>{number = 1, name = main}
2018-07-05 17:25:54.130242+0800 NSOperationDemo[22605:1156299] end---<NSThread: 0x60400007dc80>{number = 1, name = main}
2018-07-05 17:26:06.135597+0800 NSOperationDemo[22605:1156399] 1---<NSThread: 0x60400027c180>{number = 3, name = (null)}
2018-07-05 17:26:08.137346+0800 NSOperationDemo[22605:1156399] 1---<NSThread: 0x60400027c180>{number = 3, name = (null)}
2018-07-05 17:26:10.137834+0800 NSOperationDemo[22605:1156749] 2---<NSThread: 0x600000463e40>{number = 4, name = (null)}
2018-07-05 17:26:12.138668+0800 NSOperationDemo[22605:1156749] 2---<NSThread: 0x600000463e40>{number = 4, name = (null)}
*/
-(void)operationDependency2{
NSOperationQueue *oq = [[NSOperationQueue alloc]init];
NSLog(@"start---%@", [NSThread currentThread]);
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op2 addDependency:op1];
[oq addOperation:op2];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[oq addOperation:op1];
});
NSLog(@"end---%@", [NSThread currentThread]);
}
結(jié)果解釋:
op2依賴op1, 因此op2先被添加到queue中,但是op2是非ready狀態(tài),因此不會執(zhí)行, 當(dāng)10s以后,op1被添加到queue中, op1沒有依賴, 是ready狀態(tài),會直接調(diào)度執(zhí)行, 當(dāng)執(zhí)行完成, op2變成ready狀態(tài), op2執(zhí)行.
- 依賴關(guān)系是單項的, 例如
[A addDependency:B], 那么表示A依賴B, B不依賴A.- NSOperation中的依賴關(guān)系是Operation自己內(nèi)部管理的, 存儲在NSOperation的屬性
dependency中, 與具體添加到哪個Queue無關(guān).- 多個Operation之間不要建立循環(huán)依賴, 會導(dǎo)致他們都不會執(zhí)行.
- 在配置Operation的依賴是在加入到OperationQueue之前, 加入到Queue以后,配置依賴可能會失效.
- Operation的依賴關(guān)系控制這Operation的
isReady屬性.
NSOperation的優(yōu)先級
NSOperation有優(yōu)先級屬性:
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
上邊我們說過:
對于添加到隊列中的操作,首先進(jìn)入準(zhǔn)備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進(jìn)入就緒狀態(tài)的操作的開始執(zhí)行順序(非結(jié)束執(zhí)行順序)由操作之間相對的優(yōu)先級決定(優(yōu)先級是操作對象自身的屬性).
/**
2018-07-05 17:54:27.764623+0800 NSOperationDemo[23666:1216301] start---<NSThread: 0x600000079400>{number = 1, name = main}
2018-07-05 17:54:27.765324+0800 NSOperationDemo[23666:1216301] end---<NSThread: 0x600000079400>{number = 1, name = main}
2018-07-05 17:54:29.770351+0800 NSOperationDemo[23666:1216417] 1---<NSThread: 0x60000046a780>{number = 3, name = (null)}
2018-07-05 17:54:29.770354+0800 NSOperationDemo[23666:1216421] 2---<NSThread: 0x60000046a900>{number = 4, name = (null)}
2018-07-05 17:54:31.771559+0800 NSOperationDemo[23666:1216421] 2---<NSThread: 0x60000046a900>{number = 4, name = (null)}
2018-07-05 17:54:31.771560+0800 NSOperationDemo[23666:1216417] 1---<NSThread: 0x60000046a780>{number = 3, name = (null)}
*/
-(void)operationPriority1{
NSOperationQueue *oq = [[NSOperationQueue alloc]init];
[oq setMaxConcurrentOperationCount:3];
NSLog(@"start---%@", [NSThread currentThread]);
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}];
[op1 setQueuePriority:NSOperationQueuePriorityVeryHigh];
[op2 setQueuePriority:NSOperationQueuePriorityVeryLow];
[oq addOperation:op2];
[oq addOperation:op1];
NSLog(@"end---%@", [NSThread currentThread]);
}
注意
- 優(yōu)先級決定了都是處于 ready 狀態(tài)下多個operation操作之間開始的執(zhí)行順序. 執(zhí)行順序的第一要素是
isReady狀態(tài), 第二要素是他們的優(yōu)先級.- operation的優(yōu)先級只能應(yīng)用與相同的 operation queue中的 operation之間. 不同的queue中的operation不受影響.
- 如果queue是串行隊列, operation執(zhí)行順序還是按照加入到queue的先后順序執(zhí)行.
NSOperation的CompletionBlock
實際在所有的NSOperaiton中都能設(shè)置這個屬性, NSOperation會在它的主任務(wù)完成時, 回調(diào)一個completionBlock. 我們可以用 completion block 來執(zhí)行一些主任務(wù)之外的工作,比如,我們可以用它來通知一些客戶 operation 已經(jīng)執(zhí)行完畢,而并發(fā)的 operation 也可以用這個 block 來生成最終的 KVO 通知。
注意,當(dāng)一個 operation 被取消時,它的 completion block 仍然會執(zhí)行,所以我們需要在真正執(zhí)行代碼前檢查一下 isCancelled 方法的返回值。另外,我們也沒有辦法保證 completion block 被回調(diào)時一定是在主線程,理論上它應(yīng)該是與觸發(fā) isFinished 的 KVO 通知所在的線程一致的,所以如果有必要的話我們可以在 completion block 中使用 GCD 來保證從主線程更新 UI 。
completion 在NSOperation的實現(xiàn)是通過監(jiān)聽
isFinishedKVO來完成調(diào)用的.因此調(diào)用的線程與isFinished被設(shè)置的線程相關(guān). 并且在completionBlock中我們也需要判斷isCancelled來決定 Operaiton是否真正的完成.
取消NSOperation
一旦NSOperation被添加到NSOperationQueue以后, 這個NSOperation的所有權(quán)就是NSOperationQueue的, 并且不能被移除.
唯一一個控制NSOperation的操作就是cancel, 或者間接調(diào)用NSOperationQueue的cancelAllOperations.
cancel 的本質(zhì)是將NSOperation的isCancelled屬性設(shè)置為YES.
理論上, 我們會實時檢測isCancelled屬性, 在該屬性被設(shè)置成YES以后, 會將isFinished設(shè)置成YES(如果是自定義的NSOperation, 這部分代碼需要我們完成), 這樣, KVO就會發(fā)出通知, 依賴該NSOperation的其他Operation就會將isReady屬性設(shè)置成YES, 自己的 completionBlock 也會執(zhí)行.
圖片下載處理的實例
//1. 創(chuàng)建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block UIImage *image1 = NULL;
//2. 創(chuàng)建第1個下載Operation
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
image1 = [UIImage imageWithData:data];
}];
__block UIImage *image2 = NULL;
//3. 創(chuàng)建第2個下載Operation
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://pic38.nipic.com/20140228/5571398_215900721128_2.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
image2 = [UIImage imageWithData:data];
}];
// 4. 創(chuàng)建下載完成以后的繪圖操作
NSBlockOperation *combine = [NSBlockOperation blockOperationWithBlock:^{
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
[image1 drawInRect:CGRectMake(0, 0, 50, 100)];
image1 = nil;
[image2 drawInRect:CGRectMake(50, 0, 50, 100)];
image2 = nil;
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
// 5. 設(shè)置繪圖操作依賴兩個下載操作
[combine addDependency:download1];
[combine addDependency:download2];
// 6. 開始執(zhí)行任務(wù)
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combine];
}
自定義NSOperation
如果系統(tǒng)提供的兩個Operation的實現(xiàn)類不能滿足我們的需求, 我們就需要自定義NSOperation.
一般而言, 我們都是需要定義并發(fā)執(zhí)行的NSOperation. 因此我們需要重寫NSOperation的部分方法, 例如 main方法, demo如下:
@implementation PPOperation
-(void)main{
// 支持取消的Operation
if(self.isCancelled == YES){
return;
}
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"3---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
}
當(dāng)Operation開始執(zhí)行以后, 它會一直執(zhí)行, 直到它的任務(wù)執(zhí)行完成finished, 或者被取消cancelled為止. 在實際開發(fā)中, 我們的Operation可能需要在任何時間點取消這個操作, 可能在Operation被執(zhí)行之前, 可能在Operation正在運行main函數(shù)之中的某個時間點. 因此如果我們需要自定義的Operation能夠完美的支持取消操作,減少不必要的CPU消耗, 我們需要在Operation執(zhí)行期間, 定期的檢查isCancelled屬性, 一旦Operation被cancelled, 我們就需要立即停止Operation.
一般來說, 有以下幾個常規(guī)點去獲取isCancelled的值:
- 在Operation開始執(zhí)行時
- 至少在每次循環(huán)中檢查一次
- 在執(zhí)行一個耗時任務(wù)之前
- 在任何相對來說比較容易終止operation的地方
apple doc 提示 isCancelled屬性非常輕量, 不會對系統(tǒng)造成負(fù)擔(dān).
NSOperation的cancel函數(shù)并不是立即將operation取消掉,而是設(shè)置
isCancelled屬性.
手動調(diào)用start并且并發(fā)執(zhí)行的NSOperation
默認(rèn)情況下, NSOperation直接調(diào)用start是同步執(zhí)行的, 也就是說, 實際上是調(diào)用的start方法的線程中執(zhí)行的任務(wù).
如果我們需要異步執(zhí)行operation, 并且又是手動執(zhí)行(直接調(diào)用start), 因此我們需要完成以下步驟:
-
start: 必須重寫, 所有并發(fā)執(zhí)行的operation都需要重寫該方法.(并且不要調(diào)用[super start]).start方法是NSOperation任務(wù)的起點, 我們可以在這里配置operatioin的執(zhí)行線程以及其他的context. -
main: 可選.通常這個方法是專門用來實現(xiàn)與operation關(guān)聯(lián)的任務(wù)的. 盡管大多數(shù)情況,我們可以在start中實現(xiàn)任務(wù), 但在main實現(xiàn)具體任務(wù)做到控制邏輯和業(yè)務(wù)邏輯分離也很好.(SDWebImage 直接在start中完成的控制邏輯和業(yè)務(wù)邏輯調(diào)用) -
isExecuting和isFinished: 必須. 并發(fā)NSOPeration需要配置它的執(zhí)行環(huán)境, 并且對外需要支持KVO監(jiān)聽這兩個狀態(tài). -
isConcurrentorasynchronous: 必須. 這個屬性用來標(biāo)志一個operation是否是并發(fā).
基礎(chǔ)框架如下:
#import "PPOperation2.h"
@interface PPOperation2()
// 聲明屬性(父類雖然有, 但是最后重新聲明)
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@end
@implementation PPOperation2
// 手動合成兩個實例變量 _executing, _finished
@synthesize executing = _executing;
@synthesize finished = _finished;
- (id)init {
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
// finished 和 excuting 的 setter 需要通過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"];
}
@end
在重寫start, main方法以及業(yè)務(wù)代碼以后:
@interface PPOperation2()
// 聲明屬性(父類雖然有, 但是最后重新聲明)
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@end
@implementation PPOperation2
// 手動合成兩個實例變量 _executing, _finished, 因為父類設(shè)置成ReadOnly
@synthesize executing = _executing;
@synthesize finished = _finished;
- (id)init {
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
// finished 和 excuting 的 setter 需要通過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"];
}
/**
我們這里實現(xiàn)控制邏輯與業(yè)務(wù)邏輯的分離.
在start方法執(zhí)行時, 也就是具體的業(yè)務(wù)代碼`main`執(zhí)行之前, 我們判斷isCancelled方法,如果成功執(zhí)行, 我們將executing設(shè)置成YES(內(nèi)部包含KVO相關(guān)內(nèi)容)
*/
-(void)start{
if (self.isCancelled) {
self.finished = YES;
return;
}
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
self.executing = YES;
}
/**
具體的業(yè)務(wù)執(zhí)行內(nèi)容, 如果業(yè)務(wù)邏輯執(zhí)行
*/
- (void)main {
NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);
for (int i = 0; i < 2; i++) {
// 在一次循環(huán)之前檢查, 檢查是否被取消
if (self.isCancelled) {
self.executing = NO;
self.finished = YES;
return;
}
[NSThread sleepForTimeInterval:2];
NSLog(@"業(yè)務(wù)邏輯執(zhí)行---%@",[NSThread currentThread]); // 打印當(dāng)前線程
}
// 在所有任務(wù)完成以后. 設(shè)置NSOperation狀態(tài)
self.executing = NO;
self.finished = YES;
NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
}
@end
注意, 即使幾個operation被cancel調(diào)用, 仍然需要手動觸發(fā)isFinished的KVO. 因為當(dāng)一個operation依賴其他operation的時候, 它的finished 屬性會被KVO建通, 只有當(dāng)它所依賴的所有的operation的isFinished被設(shè)置成YES時, 這個operation才會執(zhí)行.
start方法主要影響的是isExecuting和isFinished
完整的需要保證KVO的屬性:
- isCancelled
- isConcurrent
- isExecuting
- isFinished
- isReady
- dependencies
- queuePriority
- completionBlock
SDWebImage中關(guān)于NSOperation的實踐
SDWebImageDownloaderOperation就是自定義NSOperatoin:
@interface SDWebImageDownloaderOperation ()
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
...
@end
@implementation SDWebImageDownloaderOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
...
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
...
self.executing = YES;
}
}
- (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)isConcurrent {
return YES;
}
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
// @synchronized 中完成
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.dataTask) {
[self.dataTask cancel];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
});
// As we cancelled the task, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
// 執(zhí)行以后, 調(diào)用done
- (void)done {
self.finished = YES;
self.executing = NO;
[self reset];
}
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// 使用 self.dataTask = nil, delegate線程寫入 dataTask
@synchronized(self) {
self.dataTask = nil;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
}
});
}
...
}
...
@end
其中整個框架就是我們自定義NSOperaiton需要去實現(xiàn)的內(nèi)容.并且有以下知識點值得學(xué)習(xí):
-
start,cancel, 內(nèi)部都用@synchronized包裝, 防止外部多個線程調(diào)用該方法.(實際場景中, 可能出現(xiàn)多個地方同時調(diào)用cancel的情況). 并且在cancel中有調(diào)用[self.dataTask cancel], 該方法會觸發(fā)URLSession:task:didCompleteWithError回調(diào)方法, 內(nèi)部因此也需要調(diào)用@synchronized(self) {}包裝dataTask相關(guān)的處理, 這里會有多線程風(fēng)險(SDWebImageDownloadStopNotification只能執(zhí)行一次.) - 只在
start方法中判斷isCancelled - 在所有完成的地方調(diào)用
done, 修改excuting/finish屬性. - 在結(jié)果處理中, 用
synchronized保護(hù)結(jié)果, 不受race condition影響.
關(guān)于下載庫, 后面都可以參考SDWebImage中Downloader.
AFNetworking 2.X版本中的NSOperation
typedef NS_ENUM(NSInteger, AFOperationState) {
AFOperationPausedState = -1,
AFOperationReadyState = 1,
AFOperationExecutingState = 2,
AFOperationFinishedState = 3,
};
static inline NSString * AFKeyPathFromOperationState(AFOperationState state) {
switch (state) {
case AFOperationReadyState:
return @"isReady";
case AFOperationExecutingState:
return @"isExecuting";
case AFOperationFinishedState:
return @"isFinished";
case AFOperationPausedState:
return @"isPaused";
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
return @"state";
#pragma clang diagnostic pop
}
}
}
@interface AFURLConnectionOperation ()
@property (readwrite, nonatomic, assign) AFOperationState state;
@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
...
- (void)operationDidStart;
- (void)finish;
- (void)cancelConnection;
@end
@implementation AFURLConnectionOperation
- (instancetype)initWithRequest:(NSURLRequest *)urlRequest {
NSParameterAssert(urlRequest);
self = [super init];
if (!self) {
return nil;
}
_state = AFOperationReadyState;
return self;
}
...
- (BOOL)isReady {
return self.state == AFOperationReadyState && [super isReady];
}
- (BOOL)isExecuting {
return self.state == AFOperationExecutingState;
}
- (BOOL)isFinished {
return self.state == AFOperationFinishedState;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isPaused {
return self.state == AFOperationPausedState;
}
- (void)setState:(AFOperationState)state {
if (!AFStateTransitionIsValid(self.state, state, [self isCancelled])) {
return;
}
[self.lock lock];
NSString *oldStateKey = AFKeyPathFromOperationState(self.state);
NSString *newStateKey = AFKeyPathFromOperationState(state);
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
[self.lock unlock];
}
- (void)resume {
if (![self isPaused]) {
return;
}
[self.lock lock];
self.state = AFOperationReadyState;
[self start];
[self.lock unlock];
}
- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
block();
});
dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}
// start 方法也需要加鎖
- (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];
}
- (void)operationDidStart {
[self.lock lock];
if (![self isCancelled]) {
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for (NSString *runLoopMode in self.runLoopModes) {
[self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
[self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
}
[self.outputStream open];
[self.connection start];
}
[self.lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
});
}
- (void)finish {
[self.lock lock];
self.state = AFOperationFinishedState;
[self.lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
});
}
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
}
[self.lock unlock];
}
- (void)cancelConnection {
NSDictionary *userInfo = nil;
if ([self.request URL]) {
userInfo = @{NSURLErrorFailingURLErrorKey : [self.request URL]};
}
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
if (![self isFinished]) {
if (self.connection) {
[self.connection cancel];
[self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
} else {
// Accommodate race condition where `self.connection` has not yet been set before cancellation
self.error = error;
[self finish];
}
}
}
總結(jié):
- 內(nèi)部維護(hù)了一組state, 用來完成
isReady,isExecuting,isFinished等關(guān)鍵屬性. (注意, 這里沒用KVO) - 在關(guān)鍵方法中
start,operationDidStart,finish,cancel方法中, 涉及到 state操作的地方.用NSRecursiveLock遞歸鎖加鎖.(所有給state賦值的地方都有加鎖) - 由于NSOperation狀態(tài)的KVO非常重要, 因此在
setState中, 維護(hù)了狀態(tài)的KVO. 這里一次性完成兩個狀態(tài)的改變 - 對于completionBlock, 部分業(yè)務(wù)邏輯需要在里面完成, 里需重寫實現(xiàn)
setCompletionBlock
注意: AFNetworking 新版本已經(jīng)棄用. 改成NSURLSession了.
在我們自定義的NSOperation中, 也可以完成自定義 state, 用這種方式完成當(dāng)前的需求.
參考文章
- iOS多線程:『NSOperation、NSOperationQueue』詳盡總結(jié)
- 知其然亦知其所以然--NSOperation并發(fā)編程
- 4.4 多線程進(jìn)階篇<下>(NSOperation)
- iOS 并發(fā)編程之 Operation Queues
- NSOperation, NSOperationQueue 原理探析
- iOS并行開發(fā):從NSOperation和調(diào)度隊列開始
- WWDC 2015 - Session 226 - Advanced NSOperations