iOS多執行緒:NSOperation詳解

夜幕降臨耶發表於2020-03-19

多執行緒開發是日常開發任務中不可缺少的一部分,在iOS開發中常用到的多執行緒開發技術有GCD、NSOperation、NSThread,本文主要講解多線系列文章中關於NSOperation的相關知識和使用詳解。

  1. iOS多執行緒:GCD詳解
  2. iOS多執行緒:NSOperation詳解

1、NSOperation簡介

NSOperation是蘋果公司提供的一套完整的多執行緒解決方案,實際上它是基於GCD更高一層的封裝,完全物件導向。相對於GCD而言使用更加的簡單、程式碼更具可讀性。包括網路請求、圖片壓縮在內的諸多多執行緒任務案例都很好的使用了NSOperation。當然NSOperation還需要NSOperationQueue這一重要角色配合使用。

  1. 支援在操作物件之間依賴關係,方便控制執行順序。
  2. 支援可選的完成塊,它在操作的主要任務完成後執行。
  3. 支援使用KVO通知監視操作執行狀態的變化。
  4. 支援設定操作的優先順序,從而影響它們的相對執行順序。
  5. 支援取消操作,允許您在操作執行時暫停操作。

2、NSOperation任務和佇列

2.1、NSOperation任務

和GCD一樣NSOperation同樣有任務的概念。所謂任務就是線上程中執行的那段程式碼,在GCD中是放在block執行的,而在NSOperation中是在其子類 NSInvocationOperationNSBlockOperation自定義子類中執行的。和GCD不同的是NSOperation需要NSOperationQueue的配合來實現多執行緒,NSOperation 單獨使用時是同步執行操作,配合 NSOperationQueue 才能實現非同步執行。

2.2、NSOperation佇列

NSOperation中的佇列是用NSOperationQueue表示的,用過來存放任務的佇列。

  • 不同於GCD中佇列先進先出的原則,對於新增到NSOperationQueue佇列中的任務,首先根據任務之間的依賴關係決定任務的就緒狀態,然後進入就緒狀態的任務由任務之間的相對優先順序決定開始執行順序。
  • 同時NSOperationQueue提供設定最大併發任務數的途徑。
  • NSOperationQueue還提供了兩種不同型別的佇列:主佇列和自定義佇列。主佇列執行在主執行緒之上,而自定義佇列在後臺執行。

3、NSOperation的基本使用

NSOperation是一個抽象類,為了做任何有用的工作,它必須被子類化。儘管這個類是抽象的,但它給了它的子類一個十分有用而且執行緒安全的方式來建立狀態、優先順序、依賴性和取消等的模型。NSOperation提供了三種方式來建立任務。 1、使用子類 NSInvocationOperation; 2、使用子類 NSBlockOperation; 3、自定義繼承自 NSOperation 的子類,通過實現內部相應的方法來封裝操作。

下面我們先來看下NSOperation上面三種不同方式的單獨使用情況。

3.1、NSInvocationOperation

NSInvocationOperation類是NSOperation的一個具體子類,當執行時,它呼叫指定物件上指定的方法。使用此類可避免為應用程式中的每個任務定義大量自定義操作物件。

-(void)invocationOperation{
    NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation) object:nil];
    [operation start];
}

-(void)operation{
    for (int i = 0; i < 5; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"%d--%@",i,[NSThread currentThread]);
    }
}

複製程式碼

列印結果: 2020-03-19 17:09:46.189458+0800 ThreadDemo[44995:12677738] 0--<NSThread: 0x600000ba9e40>{number = 1, name = main} 2020-03-19 17:09:48.190629+0800 ThreadDemo[44995:12677738] 1--<NSThread: 0x600000ba9e40>{number = 1, name = main} 2020-03-19 17:09:50.191219+0800 ThreadDemo[44995:12677738] 2--<NSThread: 0x600000ba9e40>{number = 1, name = main} 2020-03-19 17:09:52.192556+0800 ThreadDemo[44995:12677738] 3--<NSThread: 0x600000ba9e40>{number = 1, name = main} 2020-03-19 17:09:54.193900+0800 ThreadDemo[44995:12677738] 4--<NSThread: 0x600000ba9e40>{number = 1, name = main}

正如上面程式碼執行的結果顯示,NSInvocationOperation單獨使用時,並沒有開啟新的執行緒,任務都是在當前執行緒中執行的。

3.2、NSBlockOperation

NSBlockOperation類是NSOperation的一個具體子類,它充當一個或多個塊物件的包裝。該類為已經使用操作佇列且不希望建立分派佇列的應用程式提供了物件導向的包裝器。您還可以使用塊操作來利用操作依賴、KVO通知和其他可能與排程佇列不可用的特性。

-(void)blockOperationDemo{
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d--%@",i,[NSThread currentThread]);
        }
    }];
    [operation start];
}
複製程式碼

列印結果: 2020-03-19 17:19:38.673513+0800 ThreadDemo[45160:12689966] 0--<NSThread: 0x600001081100>{number = 1, name = main} 2020-03-19 17:19:40.675074+0800 ThreadDemo[45160:12689966] 1--<NSThread: 0x600001081100>{number = 1, name = main} 2020-03-19 17:19:42.676649+0800 ThreadDemo[45160:12689966] 2--<NSThread: 0x600001081100>{number = 1, name = main} 2020-03-19 17:19:44.677073+0800 ThreadDemo[45160:12689966] 3--<NSThread: 0x600001081100>{number = 1, name = main} 2020-03-19 17:19:46.677379+0800 ThreadDemo[45160:12689966] 4--<NSThread: 0x600001081100>{number = 1, name = main}

如上面程式碼執行結果所示,NSBlockOperation單獨使用時,並未開啟新的執行緒,任務的執行都是在當前執行緒中執行的。

在NSBlockOperation類中還提供一個 addExecutionBlock方法,這個方法可以新增一個程式碼執行塊,當需要執行NSBlockOperation物件時,該物件將其所有塊提交給預設優先順序的併發排程佇列。然後物件等待,直到所有的塊完成執行。當最後一個塊完成執行時,操作物件將自己標記為已完成。因此,我們可以使用塊操作來跟蹤一組執行的塊,這與使用執行緒連線來合併來自多個執行緒的結果非常相似。不同之處在於,由於塊操作本身在單獨的執行緒上執行,所以應用程式的其他執行緒可以在等待塊操作完成的同時繼續工作。需要說明的一點是,如果新增的任務較多的話,這些操作(包括 blockOperationWithBlock 中的操作)可能在不同的執行緒中併發執行,這是由系統決定的。

- (void)blockOperationDemo {
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"blockOperation--%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"executionBlock1--%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"executionBlock2--%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"executionBlock3--%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"executionBlock4--%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"executionBlock5--%@", [NSThread currentThread]);
        }
    }];
    [operation start];
}
複製程式碼

列印結果: 2020-03-19 17:40:08.102543+0800 ThreadDemo[45536:12708941] executionBlock4--<NSThread: 0x600002a1ab00>{number = 1, name = main} 2020-03-19 17:40:08.102555+0800 ThreadDemo[45536:12709185] executionBlock2--<NSThread: 0x600002a57b80>{number = 8, name = (null)} 2020-03-19 17:40:08.102555+0800 ThreadDemo[45536:12709191] executionBlock5--<NSThread: 0x600002ab8980>{number = 9, name = (null)} 2020-03-19 17:40:08.102566+0800 ThreadDemo[45536:12709186] executionBlock3--<NSThread: 0x600002a7d440>{number = 4, name = (null)} 2020-03-19 17:40:08.102570+0800 ThreadDemo[45536:12709184] executionBlock1--<NSThread: 0x600002a3aa80>{number = 6, name = (null)} 2020-03-19 17:40:08.102576+0800 ThreadDemo[45536:12709187] blockOperation--<NSThread: 0x600002a7d600>{number = 5, name = (null)} 2020-03-19 17:40:10.103970+0800 ThreadDemo[45536:12709187] blockOperation--<NSThread: 0x600002a7d600>{number = 5, name = (null)} 2020-03-19 17:40:10.103970+0800 ThreadDemo[45536:12708941] executionBlock4--<NSThread: 0x600002a1ab00>{number = 1, name = main} 2020-03-19 17:40:10.103970+0800 ThreadDemo[45536:12709185] executionBlock2--<NSThread: 0x600002a57b80>{number = 8, name = (null)} 2020-03-19 17:40:10.103980+0800 ThreadDemo[45536:12709191] executionBlock5--<NSThread: 0x600002ab8980>{number = 9, name = (null)} 2020-03-19 17:40:10.103971+0800 ThreadDemo[45536:12709186] executionBlock3--<NSThread: 0x600002a7d440>{number = 4, name = (null)} 2020-03-19 17:40:10.103973+0800 ThreadDemo[45536:12709184] executionBlock1--<NSThread: 0x600002a3aa80>{number = 6, name = (null)}

正如上面程式碼執行結果所示,在呼叫了addExecutionBlock方法新增了組個多的任務後,開啟新的執行緒,任務是併發執行的,blockOperationWithBlock中的任務執行也不是在當前的執行緒執行的。

3.3、自定義 NSOperation 的子類

如果使用子類 NSInvocationOperation、NSBlockOperation 不能滿足日常需求,我們還可以自定義子類。定一個類繼承自NSOperation,重寫它的main或者start方法便可。

@interface CustomerOperation : NSOperation

@end

@implementation CustomerOperation
- (void)main{
    if(!self.isCancelled){
        for (int i = 0; i < 4; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d--%@",i,[NSThread currentThread]);
        }
    }
}

-(void)customerOperation{
    CustomerOperation *operation = [[CustomerOperation alloc]init];
    [operation start];
}
複製程式碼

列印結果: 2020-03-19 20:28:54.473676+0800 ThreadDemo[47267:12811915] 0--<NSThread: 0x600001289040>{number = 1, name = main} 2020-03-19 20:28:56.474363+0800 ThreadDemo[47267:12811915] 1--<NSThread: 0x600001289040>{number = 1, name = main} 2020-03-19 20:28:58.474708+0800 ThreadDemo[47267:12811915] 2--<NSThread: 0x600001289040>{number = 1, name = main} 2020-03-19 20:29:00.476058+0800 ThreadDemo[47267:12811915] 3--<NSThread: 0x600001289040>{number = 1, name = main}

從上面程式碼執行結果顯示可以看出,自定義的Operation並沒有開啟新的執行緒,任務的執行是在當前的執行緒中執行的。

上面講解了NSOperation單獨使用的情況,下面我們來看看NSOperationQueue佇列配合NSOperation的使用情況。

3.4、新增任務到佇列

在上面就已經提及過,NSOperation需要NSOperationQueue來配合使用實現多執行緒。那麼我們就需要將建立好的NSOperation物件載入到NSOperationQueue佇列中。 NSOperationQueue提供了主佇列和自定義隊裡兩種佇列,其中自定義佇列中包含了序列和併發兩種不同的功能。

  • 主佇列:通過[NSOperationQueue mainQueue]方式獲取,凡是新增到主佇列中的任務都會放到主執行緒中執行。
  • 自定義佇列:通過[[NSOperationQueue alloc] init]方式建立一個佇列,凡是新增到自定義佇列中的任務會自動放到子執行緒中執行。
3.4.1、addOperation

呼叫addOperation方法將建立的operation物件新增到建立好的佇列中。

- (void)operationQueue {
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation1--%@", [NSThread currentThread]);
        }
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation2--%@", [NSThread currentThread]);
        }
    }];
    [queue addOperation:operation1];
    [queue addOperation:operation2];
}
複製程式碼

列印結果: 2020-03-19 21:01:45.868610+0800 ThreadDemo[47889:12843365] operation1--<NSThread: 0x6000012cd900>{number = 3, name = (null)} 2020-03-19 21:01:45.868610+0800 ThreadDemo[47889:12843364] operation2--<NSThread: 0x6000012e0640>{number = 6, name = (null)} 2020-03-19 21:01:47.872040+0800 ThreadDemo[47889:12843365] operation1--<NSThread: 0x6000012cd900>{number = 3, name = (null)} 2020-03-19 21:01:47.872040+0800 ThreadDemo[47889:12843364] operation2--<NSThread: 0x6000012e0640>{number = 6, name = (null)}

從上面程式碼執行的結果可以看出,開啟了新的執行緒,任務是併發執行的。如果將queue換成是mainQueue,那麼任務將會在主執行緒中同步執行。

3.4.2、addOperations

如果任務很多時,一個個新增到佇列不免有些麻煩,那麼addOperations就起作用了。

- (void)operationQueue {
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation1--%@", [NSThread currentThread]);
        }
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation2--%@", [NSThread currentThread]);
        }
    }];
    NSArray *operationList = @[operation1,operation2];
    [queue addOperations:operationList waitUntilFinished:NO];
    NSLog(@"end");
}
複製程式碼

列印結果: 2020-03-19 21:06:30.381594+0800 ThreadDemo[48047:12849411] end 2020-03-19 21:06:32.385653+0800 ThreadDemo[48047:12849496] operation1--<NSThread: 0x600001f4e880>{number = 8, name = (null)} 2020-03-19 21:06:32.385651+0800 ThreadDemo[48047:12849498] operation2--<NSThread: 0x600001fac740>{number = 4, name = (null)} 2020-03-19 21:06:34.390373+0800 ThreadDemo[48047:12849496] operation1--<NSThread: 0x600001f4e880>{number = 8, name = (null)} 2020-03-19 21:06:34.390373+0800 ThreadDemo[48047:12849498] operation2--<NSThread: 0x600001fac740>{number = 4, name = (null)}

從上面程式碼執行的記過可以看出,開啟了新的執行緒,任務是併發執行的。如果將queue換成是mainQueue,那麼任務將會在主執行緒中同步執行。

這裡需要說明的一點的是waitUntilFinished引數,如果傳YES,則表示會等待佇列裡面的任務執行完成後才會往下執行,也就是會阻塞執行緒。

3.4.3、addOperationWithBlock

NSOperationQueue還提供了一個addOperationWithBlock方法可以將operation物件新增到NSOperationQueue中。

-(void)addOperationWithBlock{
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//        NSOperationQueue *queue = [NSOperationQueue mainQueue];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation1--%@", [NSThread currentThread]);
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation2--%@", [NSThread currentThread]);
        }
    }];
}
複製程式碼

列印結果: 2020-03-19 21:11:54.069593+0800 ThreadDemo[48192:12856146] operation1--<NSThread: 0x600000b0f740>{number = 4, name = (null)} 2020-03-19 21:11:54.069593+0800 ThreadDemo[48192:12856148] operation2--<NSThread: 0x600000b324c0>{number = 3, name = (null)} 2020-03-19 21:11:56.070432+0800 ThreadDemo[48192:12856148] operation2--<NSThread: 0x600000b324c0>{number = 3, name = (null)} 2020-03-19 21:11:56.070430+0800 ThreadDemo[48192:12856146] operation1--<NSThread: 0x600000b0f740>{number = 4, name = (null)}

從上面程式碼執行的記過可以看出,開啟了新的執行緒,任務是併發執行的。如果將queue換成是mainQueue,那麼任務將會在主執行緒中同步執行。

3.5、同步執行&併發執行

在前面的內容已經提及過,NSOperation單獨使用時預設是系統同步執行的,如果需要併發執行任務,就需要NSOperationQueue的協同。那麼決定是併發執行還是同步執行的關鍵就在於最大併發任務數maxConcurrentOperationCount

  • 預設情況下maxConcurrentOperationCount的值是-1,並不做限制,可以併發執行,如上面提到的NSBlockOperation新增多個任務塊。
  • maxConcurrentOperationCount的值為1時,同步執行。
  • maxConcurrentOperationCount的值大於1時,併發執行。
  • maxConcurrentOperationCount的值並不是表示併發執行的執行緒數量,而是在一個佇列中能夠同時執行的任務的數量。
- (void)maxConcurrentOperationCount {
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    queue.maxConcurrentOperationCount = 1;//序列佇列
//    queue.maxConcurrentOperationCount = 4;//併發佇列
    NSLog(@"maxCount=%ld", (long)queue.maxConcurrentOperationCount);
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation1--%@", [NSThread currentThread]);
        }
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation2--%@", [NSThread currentThread]);
        }
    }];
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"operation3--%@", [NSThread currentThread]);
        }
    }];
    NSArray *operationList = @[operation1, operation2, operation3];
    [queue addOperations:operationList waitUntilFinished:YES];

    NSLog(@"end");
}
複製程式碼

列印結果: 2020-03-19 21:35:41.878534+0800 ThreadDemo[48619:12879620] maxCount=1 2020-03-19 21:35:43.882396+0800 ThreadDemo[48619:12879824] operation1--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:45.882889+0800 ThreadDemo[48619:12879824] operation1--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:47.886984+0800 ThreadDemo[48619:12879824] operation2--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:49.888093+0800 ThreadDemo[48619:12879824] operation2--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:51.893354+0800 ThreadDemo[48619:12879824] operation3--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:53.894355+0800 ThreadDemo[48619:12879824] operation3--<NSThread: 0x600000c7b240>{number = 3, name = (null)} 2020-03-19 21:35:53.894723+0800 ThreadDemo[48619:12879620] end

從上面的程式碼執行結果可以看出,開啟了新的執行緒,任務是序列執行的。

如果將maxConcurrentOperationCount的值修改為2,那麼列印的記過如下:

2020-03-19 21:36:59.126533+0800 ThreadDemo[48668:12881702] maxCount=2 2020-03-19 21:37:01.130238+0800 ThreadDemo[48668:12881793] operation1--<NSThread: 0x600003a92280>{number = 5, name = (null)} 2020-03-19 21:37:01.130246+0800 ThreadDemo[48668:12881794] operation2--<NSThread: 0x600003a45840>{number = 6, name = (null)} 2020-03-19 21:37:03.133480+0800 ThreadDemo[48668:12881793] operation1--<NSThread: 0x600003a92280>{number = 5, name = (null)} 2020-03-19 21:37:03.133489+0800 ThreadDemo[48668:12881794] operation2--<NSThread: 0x600003a45840>{number = 6, name = (null)} 2020-03-19 21:37:05.137502+0800 ThreadDemo[48668:12881794] operation3--<NSThread: 0x600003a45840>{number = 6, name = (null)} 2020-03-19 21:37:07.140419+0800 ThreadDemo[48668:12881794] operation3--<NSThread: 0x600003a45840>{number = 6, name = (null)} 2020-03-19 21:37:07.140713+0800 ThreadDemo[48668:12881702] end

從上面的執行結果可以看出,開啟了新的執行緒,任務是併發執行的,而且每次執行的任務數最大為2個,那是因為我們設定了maxConcurrentOperationCount的值為2,而新增了3個任務在佇列中。

3.6、NSOperation執行緒間的通訊

多執行緒操作可能永遠也繞不過執行緒間通訊這個話題。通常我們將耗時的操作諸如網路請求、檔案上傳下載都放在子執行緒中執行,待執行完成之後需要回到主執行緒進行UI重新整理操作,那麼就會存在主執行緒和子執行緒之間的切換問題,好在NSOperation執行緒之間的通訊是十分簡單的。

-(void)threadCommunication{
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 4; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"子執行緒--%@", [NSThread currentThread]);
        }
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2];
                NSLog(@"主執行緒--%@", [NSThread currentThread]);
            }
        }];
        
    }];
    [queue addOperation:operation];
}
複製程式碼

列印結果: 2020-03-19 21:48:12.051256+0800 ThreadDemo[48922:12893188] 子執行緒--<NSThread: 0x600000b5fa80>{number = 6, name = (null)} 2020-03-19 21:48:14.056107+0800 ThreadDemo[48922:12893188] 子執行緒--<NSThread: 0x600000b5fa80>{number = 6, name = (null)} 2020-03-19 21:48:16.059279+0800 ThreadDemo[48922:12893188] 子執行緒--<NSThread: 0x600000b5fa80>{number = 6, name = (null)} 2020-03-19 21:48:18.062773+0800 ThreadDemo[48922:12893188] 子執行緒--<NSThread: 0x600000b5fa80>{number = 6, name = (null)} 2020-03-19 21:48:20.064401+0800 ThreadDemo[48922:12893108] 主執行緒--<NSThread: 0x600000bd2d00>{number = 1, name = main} 2020-03-19 21:48:22.065409+0800 ThreadDemo[48922:12893108] 主執行緒--<NSThread: 0x600000bd2d00>{number = 1, name = main}

3.7、NSOperation 操作依賴

NSOperation最大的亮點莫過於可以新增任務之間的依賴關係。所謂的依賴關係就是任務A需要等待任務B完成之後才能繼續執行。NSOperation提供了三個方法為任務之間設定依賴關係。

  • -(void)addDependency:(NSOperation *)op; 新增依賴,使當前操作依賴於操作 op 的完成。
  • -(void)removeDependency:(NSOperation *)op; 移除依賴,取消當前操作對操作 op 的依賴。
  • NSArray<NSOperation *> *dependencies; 在當前操作開始執行之前完成執行的所有操作物件陣列。
- (void)addDependency {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@", [NSThread currentThread]);
        }
    }];
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@", [NSThread currentThread]);
        }
    }];

    // operation1依賴於operation2和operation3,則先執行operation2和operation3,然後執行operation1
    [operation1 addDependency:operation2];
    [operation1 addDependency:operation3];
    NSArray *opList = @[operation1,operation2,operation3];
    NSArray *dependencies = [operation1 dependencies];
    NSLog(@"dependencies-%@",dependencies);
    [queue addOperations:opList waitUntilFinished:YES];
    NSLog(@"end");
}
複製程式碼

列印結果: 2020-03-19 22:11:32.567850+0800 ThreadDemo[49369:12918472] dependencies-( "<NSBlockOperation: 0x7ff341a06e40>", "<NSBlockOperation: 0x7ff341a06f50>" ) 2020-03-19 22:11:34.571689+0800 ThreadDemo[49369:12918726] 3---<NSThread: 0x6000037cf180>{number = 3, name = (null)} 2020-03-19 22:11:34.571694+0800 ThreadDemo[49369:12918732] 2---<NSThread: 0x6000037fbe40>{number = 7, name = (null)} 2020-03-19 22:11:36.577098+0800 ThreadDemo[49369:12918726] 3---<NSThread: 0x6000037cf180>{number = 3, name = (null)} 2020-03-19 22:11:36.577107+0800 ThreadDemo[49369:12918732] 2---<NSThread: 0x6000037fbe40>{number = 7, name = (null)} 2020-03-19 22:11:38.582249+0800 ThreadDemo[49369:12918726] 1---<NSThread: 0x6000037cf180>{number = 3, name = (null)} 2020-03-19 22:11:40.587676+0800 ThreadDemo[49369:12918726] 1---<NSThread: 0x6000037cf180>{number = 3, name = (null)} 2020-03-19 22:11:40.587996+0800 ThreadDemo[49369:12918472] end

從上面的程式碼執行結果可以看出operation2和operation3執行完成後才去執行的operation1。

3.8、NSOperation的優先順序

NSOperation的另一個亮點就是NSOperation提供了queuePriority屬性,該屬性決定了任務在佇列中執行的順序。

  • queuePriority屬性只對同一個佇列中的任務有效。
  • queuePriority屬性不能取代依賴關係。
  • 對於進入準備就緒狀態的任務優先順序高的任務優先於優先順序低的任務。
  • 優先順序高的任務不一定會先執行,因為已經進入準備就緒狀態的任務即使是優先順序低也會先執行。
  • 新建立的operation物件的優先順序預設是NSOperationQueuePriorityNormal,可以通過setQueuePriority:方法來修改優先順序。
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
複製程式碼
- (void)addDependency {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@", [NSThread currentThread]);
        }
    }];
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@", [NSThread currentThread]);
        }
    }];

    // operation1依賴於operation2和operation3,則先執行operation2和operation3,然後執行operation1
    [operation1 addDependency:operation2];
    [operation1 addDependency:operation3];
    operation1.queuePriority = NSOperationQueuePriorityVeryHigh;
    NSArray *opList = @[operation1,operation2,operation3];
    NSArray *dependencies = [operation1 dependencies];
    NSLog(@"dependencies-%@",dependencies);
    [queue addOperations:opList waitUntilFinished:YES];
    NSLog(@"end");
}
複製程式碼

列印結果: 2020-03-19 22:31:15.086135+0800 ThreadDemo[49743:12937692] dependencies-( "<NSBlockOperation: 0x7ffa6140a980>", "<NSBlockOperation: 0x7ffa6140a760>" ) 2020-03-19 22:31:17.087052+0800 ThreadDemo[49743:12937910] 3---<NSThread: 0x6000033d1f80>{number = 5, name = (null)} 2020-03-19 22:31:17.087060+0800 ThreadDemo[49743:12937909] 2---<NSThread: 0x6000033d1780>{number = 4, name = (null)} 2020-03-19 22:31:19.087421+0800 ThreadDemo[49743:12937909] 2---<NSThread: 0x6000033d1780>{number = 4, name = (null)} 2020-03-19 22:31:19.087421+0800 ThreadDemo[49743:12937910] 3---<NSThread: 0x6000033d1f80>{number = 5, name = (null)} 2020-03-19 22:31:21.090223+0800 ThreadDemo[49743:12937910] 1---<NSThread: 0x6000033d1f80>{number = 5, name = (null)} 2020-03-19 22:31:23.092879+0800 ThreadDemo[49743:12937910] 1---<NSThread: 0x6000033d1f80>{number = 5, name = (null)} 2020-03-19 22:31:23.093183+0800 ThreadDemo[49743:12937692] end

如上程式碼執行結果所示,即使將operation1的優先順序設定為最高NSOperationQueuePriorityVeryHigh,operation1依然是最後執行的,那是因為operation1依賴於operation2和operation3,在operation2和operation3未執行完成之前,operation1一直是處於為就緒狀態,即使優先順序最高,也不會執行。

3.9、狀態

NSOperation包含了一個十分優雅的狀態機來描述每一個操作的執行。isReady → isExecuting → isFinished。為了替代不那麼清晰的state屬性,狀態直接由上面那些keypathKVO通知決定,也就是說,當一個操作在準備好被執行的時候,它傳送了一個KVO通知給isReadykeypath,讓這個keypath對應的屬性isReady在被訪問的時候返回YES。 每一個屬性對於其他的屬性必須是互相獨立不同的,也就是同時只可能有一個屬性返回YES,從而才能維護一個連續的狀態:

  • isReady: 返回YES表示操作已經準備好被執行, 如果返回NO則說明還有其他沒有先前的相關步驟沒有完成。
  • isExecuting: 返回YES表示操作正在執行,反之則沒在執行。
  • isFinished : 返回YES表示操作執行成功或者被取消了,NSOperationQueue只有當它管理的所有操作的isFinished屬性全標為YES以後操作才停止出列,也就是佇列停止執行,所以正確實現這個方法對於避免死鎖很關鍵。

3.10、其他API

  1. - (void)cancel; 可取消操作,實質是標記 isCancelled 狀態。 判斷操作狀態方法
  2. - (BOOL)isFinished; 判斷操作是否已經結束。
  3. - (BOOL)isCancelled; 判斷操作是否已經標記為取消。
  4. - (BOOL)isExecuting; 判斷操作是否正在在執行。
  5. - (BOOL)isReady;判斷操作是否處於準備就緒狀態,這個值和操作的依賴關係相關。
  6. - (void)waitUntilFinished; 阻塞當前執行緒,直到該操作結束。可用於執行緒執行順序的同步。
  7. - (void)setCompletionBlock:(void (^)(void))block; completionBlock 會在當前操作執行完畢時執行 completionBlock。
  8. - (void)cancelAllOperations;可以取消佇列的所有操作。
  9. - (BOOL)isSuspended; 判斷佇列是否處於暫停狀態。 YES 為暫停狀態,NO 為恢復狀態。10.- (void)setSuspended:(BOOL)b; 可設定操作的暫停和恢復,YES 代表暫停佇列,NO 代表恢復佇列。
  10. - (void)waitUntilAllOperationsAreFinished; 阻塞當前執行緒,直到佇列中的操作全部執行完畢。
  11. - (NSUInteger)operationCount; 當前佇列中的運算元。 獲取佇列
  12. + (id)currentQueue; 獲取當前佇列,如果當前執行緒不是在 NSOperationQueue 上執行則返回 nil。

4、NSOperation的執行緒安全

和其他多執行緒方案一樣,解決NSOperation多執行緒安全問題,可以給執行緒加鎖,在一個執行緒執行該操作的時候,不允許其他執行緒進行操作。iOS 實現執行緒加鎖有很多種方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock等等各種方式。

5、參考資料

相關文章