多線程原理--NSOperation、NSOperationQueue

NSOperation類是iOS2.0推出的,通過NSThread實現的,但是效率一般。從iOS4推出GCD時又重寫了NSOperation和NSOperationQueue,NSOperation和NSOperationQueue分別對應GCD的任務和隊列(瞭解GCD直通車:https://www.jianshu.com/p/acc6e7bd6f10),所以NSOPeration和NSOperationQueue是基於GCD更高一層的封裝,而且完全地面向對象。但是比GCD更簡單易用、代碼可讀性也更高。NSOperation和NSOperationQueue對比GCD會帶來一點額外的系統開銷,但是可以在多個操作Operation中添加附屬。

優點

  • 可添加完成的代碼塊,在操作完成後執行。
  • 添加操作之間的依賴關係,方便的控制執行順序。
  • 設定操作執行的優先級。
  • 可以很方便的取消一個操作的執行。
  • 使用 KVO 觀察對操作執行狀態的更改:isExecuteing、isFinished、isCancelled。

NSOperation、NSOperationQueue

NSOperation是一個和任務相關的抽象類,不具備封裝操作的能力,必須使用其子類:NSInvocationOperation或者NSBlockOperation,當然也可以自定義子類,實現內部相應的⽅法,NSOperation實例在多線程上執行是線程安全的不需要添加額外的鎖,不 必管理線程生命週期和同步等問題。NSInvocationOperation 和NSBlockOperation子類不同的是,因爲NSInvocationOperation沒有額外添加任務的方法,所以使用NSInvocationOperation創建的對象只會有一個任務,其次使用NSBlockOperation來執行任務切任務大於1的時候,系統可能會開闢新線程來異步執行任務。

NSOperationQueue有主隊列和自定義隊列(串行和併發),將NSOperation對象添加NSOperationQueue中,該NSOperationQueue對象從線程中拿取操作、以及分配到對應線程的工作都是由系統處理的。一般操作對象添加到NSOperationQueue之後,如果不存在依賴或者整個隊列被暫停情況下通常短時間就開始運行。NSOperationQueue可以通過對象屬性suspended來決定是否讓隊列暫時停止對任務的調度,或者cancel、cancelAllOperations方法來取消操作對象,不過使用這兩個方法後操作對象無法恢復,操作時只會停止調度隊列中操作對象(注意:正在執行的操作依然會執行,無法取消。 ), 且取消不可恢復。

首先創建一個NSOperation的子類(以NSInvocationOperation爲例),再創建隊列NSOperationQueue,最後將操作加入隊列。這樣我們就完成了多線程的操作。可以直接執行start方法,但不會開闢新線程去執行操作,而是在當前線程同步執行任務。這裏注意,如果將操作添加到隊列的同時再次執行start,這時會拋出異常,因爲線程在創建後,開始進入Runnable就緒的狀態,如果此時再次執行start會重複初始化操作。

    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"123"];

    // 將op加入到隊列中
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op];
    [[NSOperationQueue mainQueue] addOperation:op];

    // 或者直接start
    [op start];

因爲基於GCD更高一層的封裝,NSOperation同樣也具備線程之間的通訊以及控制併發數,舉個簡單的例子:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 2;
    for (int i = 0; i<10; i++) {
        [queue addOperationWithBlock:^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d-%@",i,[NSThread currentThread]);
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                NSLog(@"%d-%@--%@",i,[NSOperationQueue currentQueue],[NSThread currentThread]);
            }];
        }];
    }

由於設置了最大併發數maxConcurrentOperationCount爲2,所以每2秒輸出四個任務。

0-<NSThread: 0x600003f08180>{number = 4, name = (null)}
1-<NSThread: 0x600003f17bc0>{number = 5, name = (null)}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}
2-<NSThread: 0x600003f31e00>{number = 7, name = (null)}
3-<NSThread: 0x600003f08180>{number = 4, name = (null)}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}
... // 省略部分相似打印信息
9-<NSThread: 0x600003f08180>{number = 4, name = (null)}
8-<NSThread: 0x600003f17bc0>{number = 5, name = (null)}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}
<NSOperationQueue: 0x7fce0de147f0>{name = 'NSOperationQueue Main Queue'} --<NSThread: 0x600003f5acc0>{number = 1, name = main}

當然我們也可以自定義子類,可能會添加到串行和併發隊列的不同情況,需要重寫不同的方法。TIP:併發操作時,需要自己創建自動釋放池,因爲異步操作無法訪問主線程的自動釋放池。經常通過cancelled屬性檢查方法是否取消,並且對取消做出響應。

操作依賴

NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之間的依賴關係,可以使用依賴關係來控制操作間的啓動順序。當一個操作對象添加了依賴,被依賴的操作對象就會先執行,當被依賴的操作對象執行完纔會當前的操作對象,通過操作依賴可以很方便的按照特定順序控制操作之間的執行先後順序。操作對象可以通過addDependency添加和removeDependency移除依賴。在添加線程對象NSOperationQueue之前進行依賴設置,操作對象會管理自己的依賴,因此在不相同隊列中的操作對象可以建立依賴關係。

舉例:現在有任務1、2、3,當任務1執行完畢後再執行任務2,任務2執行完畢後再執行任務3。

    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"任務----1");
    }];
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"任務----2");
    }];
    
    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"任務----3"); 
    }];
    
    // 建立依賴
    [bo2 addDependency:bo1];
    [bo3 addDependency:bo2];

    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
    
    NSLog(@"執行結束");

這裏waitUntilFinished如果設置爲YES,則會堵塞當前線程,直到該操作結束。
最終打印效果總是:任務1->任務2->任務3

 任務----1
 任務----2
 任務----3
 執行結束

優先級、服務質量

NSOperation 提供了queuePriority(優先級)屬性,queuePriority屬性適用於同一操作隊列中的操作,不適用於不同操作隊列中的操作。默認情況下,所有新創建的操作對象優先級都是NSOperationQueuePriorityNormal。但是我們可以通過setQueuePriority方法來改變當前操作在同一隊列中的執行優先級。在iOS 8.0後,推出了服務質量,通過設置服務質量讓系統優先處理某一個操作。

NSOperation優先級的枚舉和Quality of Service枚舉:

// NSOperation.h
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

// ----------------------------
// NSObjCRuntime.h
typedef NS_ENUM(NSInteger, NSQualityOfService) {
//與用戶交互的任務,這些任務通常跟UI級別的刷新相關,比如動畫,這些任務需要在一瞬間完成.
    NSQualityOfServiceUserInteractive = 0x21,
// 由用戶發起的並且需要立即得到結果的任務,比如滑動scroll view時去加載數據用於後續cell的顯示,這些任務通常跟後續的用戶交互相關,在幾秒或者更短的時間內完成
    NSQualityOfServiceUserInitiated = 0x19,
// 一些可能需要花點時間的任務,這些任務不需要馬上返回結果,比如下載的任務,這些任務可能花費幾秒或者幾分鐘的時間
    NSQualityOfServiceUtility = 0x11,
// 一些可能需要花點時間的任務,這些任務不需要馬上返回結果
    NSQualityOfServiceBackground = 0x09,
// 一些可能需要花點時間的任務,這些任務不需要馬上返回結果
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

Utility 及以下的優先級會受到 iOS9 中低電量模式的控制,另外在沒有用戶操作時,90% 任務的優先級都應該在 Utility 之下。

一般對於添加到隊列中的任務,當這個任務的所有依賴都已經完成時,任務通常會進入準備就緒狀態來等待執行,這時該任務的開始執行順序由任務的優先級決定。如果一個隊列中既包含高優先級就緒狀態的任務,又包含低優先級就緒狀態的任務,高優先級就緒狀態的任務會優先被執行;

服務質量則根據CPU,網絡和磁盤的分配來創建一個操作的系統優先級。一個高質量的服務意味着可以提供更多的資源來更快的完成操作,涉及到CPU調度的優先級、IO優先級、任務運行所在的線程以及運行的順序等等。正確的指定線程或任務優先級可以讓系統更加智能的管理隊列的資源分配,以便於提高執行效率和控制電量等。

自定義NSOperation子類

當NSInvocationOperation或者NSBlockOperation無法滿足我們日常需求,我們也可以定義串行和併發的2種類型的NSOperation子類,注意需要創建自動釋放池,異步操作無法訪問主線程的自動釋放池。在自定義串行NSOperation子類時要重寫main方法並且最好添加一個init方法用於初始化數據,在自定義並行NSOperation子類是需要重寫start、isFinished、isAsynchronousisExecuting方法。經常通過cancelled屬性檢查方法是否取消,並且對取消的做出響應,定期調用對象的isCancelled方法,如果返回“YES”,則立即返回,不再執行任務。

如果想進一步瞭解自定義NSOperation子類的具體實現,接下來將利用自定義NSOperation子類,同時借鑑了AFNetworking、SDWebImage、YYKit的部分思想來實現具有緩存支持的異步圖像下載器。

該文章爲記錄本人的學習路程,也希望能夠幫助大家,知識共享,共同成長,共同進步!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章