總結NSOperation、NSOperationQueue

語言: CN / TW / HK

NSOperation、NSOperationQueue
是蘋果提供給我們的一套多執行緒解決方案。
是基於 GCD 更高一層的封裝,完全面向物件。但是比 GCD 更簡單易用、程式碼可讀性也更高。


為什麼要使用 NSOperation、NSOperationQueue?

  1. 可新增 在操作完成後執行的 程式碼塊 。
  2. 新增操作之間的 依賴關係,方便的 控制 執行順序。
  3. 設定操作執行的  優先順序。
  4. 可以很方便的   取消一個操作 的執行。
  5. 使用 KVO 觀察 對操作   執行狀態  的更改:isExecuteing、isFinished、isCancelled。


  • 操作(Operation):
    • 執行操作的意思,換句話說就是你在執行緒中執行的那段程式碼
    • 在 GCD 中是放在 block 中的。在 NSOperation 中,我們使用 NSOperation 子類 NSInvocationOperationNSBlockOperation,或者自定義子類來封裝操作。
  •       操作佇列(Operation Queues):
    • 這裡的佇列指操作佇列,即用來    存放操作的佇列。不同於 GCD 中的排程佇列 FIFO(先進先出)的原則。NSOperationQueue 對於新增到佇列中的操作,首先進入準備就緒的狀態(就緒狀態取決於操作之間的依賴關係),然後進入就緒狀態的操作的開始執行順序(非結束執行順序)由操作之間相對的優先順序決定(優先順序是操作物件自身的屬性)。
    • 操作佇列通過設定 最大併發運算元(maxConcurrentOperationCount) 來控制併發、序列。
    • NSOperationQueue 為我們提供了兩種不同型別的佇列:主佇列和自定義佇列主佇列執行在主執行緒之上,而自定義佇列在後臺執行

  • 使用步驟

    因為預設情況下,NSOperation 單獨使用時系統同步執行操作,
    配合 NSOperationQueue 我們能更好的實現非同步執行。

    NSOperation 實現多執行緒的使用步驟分為三步:

    1. 建立操作:先將需要執行的操作封裝到一個 NSOperation 物件中。
    2. 建立佇列:建立 NSOperationQueue 物件。
    3. 將操作加入到佇列中:將 NSOperation 物件新增到 NSOperationQueue 物件中。

    之後呢,
    系統就會自動將 NSOperationQueue 中的 NSOperation 取出來,在新執行緒中執行操作。


    基本使用

    NSOperation 是個抽象類,不能用來封裝操作。
    我們只有使用它的子類來封裝操作。我們有三種方式來封裝操作。

    1. 使用子類 NSInvocationOperation
    2. 使用子類 NSBlockOperation
    3. 自定義繼承自 NSOperation 的子類,通過實現內部相應的方法來封裝操作。

    在不使用 NSOperationQueue,單獨使用 NSOperation 的情況下系統同步執行操作,下面我們學習以下操作的三種建立方式。



    1. 使用子類 NSInvocationOperation

        // 1.建立 NSInvocationOperation 物件
    NSInvocationOperation *op = [[NSInvocationOperation alloc]     initWithTarget:self selector:@selector(task1) object:nil];
    
        // 2.呼叫 start 方法開始執行操作
        [op start];
    }
    複製程式碼


    在沒有使用 NSOperationQueue、在主執行緒中單獨使用使用子類 NSInvocationOperation 執行一個操作的情況下,操作是在當前執行緒執行的,並沒有開啟新執行緒。

    如果在其他執行緒中執行操作,則列印結果為其他執行緒。

    2. 使用子類 NSBlockOperation

        // 1.建立 NSBlockOperation 物件
        NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
    
        // 2.呼叫 start 方法開始執行操作
        [op start];
    複製程式碼
    • 可以看到:在沒有使用 NSOperationQueue、在主執行緒中單獨使用 NSBlockOperation 執行一個操作的情況下,操作是在當前執行緒執行的,並沒有開啟新執行緒。

    注意:和上邊 NSInvocationOperation 使用一樣。因為程式碼是在主執行緒中呼叫的,所以列印結果為主執行緒。如果在其他執行緒中執行操作,則列印結果為其他執行緒。

    但是,NSBlockOperation 還提供了一個方法 addExecutionBlock:,通過 addExecutionBlock: 就可以為 NSBlockOperation 新增額外的操作。這些操作(包括 blockOperationWithBlock 中的操作)可以在不同的執行緒中同時(併發)執行。只有當所有相關的操作已經完成執行時,才視為完成。

    如果新增的操作多的話,blockOperationWithBlock: 中的操作也可能會在其他執行緒(非當前執行緒)中執行,這是由系統決定的,並不是說新增到 blockOperationWithBlock: 中的操作一定會在當前執行緒中執行。(可以使用 addExecutionBlock: 多新增幾個操作試試)。

    一般情況下,如果一個 NSBlockOperation 物件封裝了多個操作。NSBlockOperation 是否開啟新執行緒,取決於操作的個數。如果新增的操作的個數多,就會自動開啟新執行緒。當然開啟的執行緒數是由系統來決定的。


    3. 使用自定義繼承自 NSOperation 的子類

    可以通過重寫 main 或者 start 方法 來定義自己的 NSOperation 物件。
    重寫main方法比較簡單,我們不需要管理操作的狀態屬性 isExecutingisFinished
    main 執行完返回的時候,這個操作就結束了。

    • 可以看出:在沒有使用 NSOperationQueue、在主執行緒單獨使用自定義繼承自 NSOperation 的子類的情況下,是在主執行緒執行操作,並沒有開啟新執行緒。



    建立佇列 

    NSOperationQueue 一共有兩種佇列:主佇列、自定義佇列。

    • 佇列
      • 凡是新增到主佇列中的操作,都會放到主執行緒中執行。

        // 主佇列獲取方法
        NSOperationQueue *queue = [NSOperationQueue mainQueue]; 複製程式碼
    • 自定義佇列(非主佇列)
      • 新增到這種佇列中的操作,就會自動放到子執行緒中執行。
      • 同時包含了:序列、併發功能。

        // 自定義佇列建立方法
        NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 複製程式碼


    將操作加入到佇列中

    總共有兩種方法:

    1. - (void)addOperation:(NSOperation *)op;
      • 需要先建立操作,再將建立好的操作加入到建立好的佇列中去

        /**
         * 使用 addOperation: 將操作加入到操作佇列中
         */
        - (void)addOperationToQueue {
        
            // 1.建立佇列
            NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        
            // 2.建立操作
            // 使用 NSInvocationOperation 建立操作1
            NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
        
            // 使用 NSInvocationOperation 建立操作2
            NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
        
            // 使用 NSBlockOperation 建立操作3
            NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                    NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒
                }
            }];
            [op3 addExecutionBlock:^{
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                    NSLog(@"4---%@", [NSThread currentThread]); // 列印當前執行緒
                }
            }];
        
            // 3.使用 addOperation: 新增所有操作到佇列中
            [queue addOperation:op1]; // [op1 start]
            [queue addOperation:op2]; // [op2 start]
            [queue addOperation:op3]; // [op3 start]
        }複製程式碼
    • 可以看出:使用 NSOperation 子類建立操作,並使用 addOperation: 將操作加入到操作佇列後能夠開啟新執行緒,進行併發執行。


    2. - (void)addOperationWithBlock:(void (^)(void))block;

     無需先建立操作,在 block 中新增操作,直接將包含操作的 block 加入到佇列中。


    /**
     * 使用 addOperationWithBlock: 將操作加入到操作佇列中
     */
    
    - (void)addOperationWithBlockToQueue {
        // 1.建立佇列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        // 2.使用 addOperationWithBlock: 新增操作到佇列中
        [queue addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
        [queue addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
        [queue addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"3---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
    }
    複製程式碼
    • 可以看出:使用 addOperationWithBlock: 將操作加入到操作佇列後能夠開啟新執行緒,進行併發執行。



    NSOperationQueue 控制序列執行、併發執行

    1, 關鍵屬性:  maxConcurrentOperationCount,叫做最大併發運算元

    用來控制一個特定佇列中可以有多少個操作同時參與併發執行。

    注意:這裡 maxConcurrentOperationCount 控制的 不是併發執行緒的數量而是一個佇列中同時能併發執行的最大運算元。而且一個操作也並非只能在一個執行緒中執行。

    最大併發運算元:maxConcurrentOperationCount

      預設情況下為-1,表示不進行限制,可進行併發執行

      為1時,佇列為序列佇列。只能序列執行。

      大於1時,佇列為併發佇列。操作併發執行,當然這個值不應超過系統限制,即使自己設定一個很大的值,系統也會自動調整為 min{自己設定的值,系統設定的預設最大值}。


    • 可以看出:當最大併發運算元為1時,操作是按順序序列執行的,並且一個操作完成之後,下一個操作才開始執行。

      當最大操作併發數為2時,操作是併發執行的,可以同時執行兩個操作。而開啟執行緒數量是由系統決定的,不需要我們來管理。

    這樣看來,是不是比 GCD 還要簡單了許多?


    NSOperation 操作依賴

    最吸引人的地方是它能新增操作之間的依賴關係。
    通過操作依賴,我們可以很方便的控制操作之間執行先後順序。
    NSOperation 提供了3個介面供我們管理和檢視依賴。

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

    比如說有 A、B 兩個操作,其中 A 執行完操作,B 才能執行操作。

    如果使用依賴來處理的話,那麼就需要讓操作 B 依賴於操作 A。


    NSOperation 優先順序

    queuePriority(優先順序)屬性,queuePriority屬性適用於同一操作佇列中的操作,不適用於不同操作佇列中的操作。

    預設情況下,所有新建立的操作物件優先順序都是NSOperationQueuePriorityNormal

    但是我們可以通過setQueuePriority:方法來改變當前操作在同一佇列中的執行優先順序。

    // 優先順序的取值
    typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
        NSOperationQueuePriorityVeryLow = -8L,
        NSOperationQueuePriorityLow = -4L,
        NSOperationQueuePriorityNormal = 0,
        NSOperationQueuePriorityHigh = 4,
        NSOperationQueuePriorityVeryHigh = 8
    };
    複製程式碼

    對於新增到佇列中的操作,
    首先進入準備就緒的狀態(就緒狀態取決於操作之間的依賴關係)
    然後進入就緒狀態的操作的 開始執行順序(非結束執行順序)由操作之間相對的優先順序決定(優先順序是操作物件自身的屬性)


    一串預設Normal優先順序的操作 ,沒有需要依賴的操作 會先進入準備就緒狀態。

    優先順序  屬性決定了進入準備就緒狀態下的操作之間的開始執行順序。
    並且,優先順序不能取代依賴關係。

    依賴關係 >   優先順序
    優先順序不能取代依賴關係。如果要控制操作間的啟動順序,則必須使用依賴關係

    NSOperation、NSOperationQueue 執行緒間的通訊

    一般在主執行緒裡邊進行 UI 重新整理,例如:點選、滾動、拖拽等事件。
    我們通常把一些耗時的操作放在其他執行緒

    當我們有時候在其他執行緒完成了耗時操作時,需要回到主執行緒,那麼就用到了執行緒之間的通訊。

    /**
     * 執行緒間通訊
     */
    - (void)communication {
    
        // 1.建立佇列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    
        // 2.新增操作
        [queue addOperationWithBlock:^{
            // 非同步進行耗時操作
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒
            }
            // 回到主執行緒
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // 進行一些 UI 重新整理等操作
                for (int i = 0; i < 2; i++) {
                    [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                    NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒
                }
            }];
        }];
    }複製程式碼

    NSOperation、NSOperationQueue 執行緒同步和執行緒安全

    • 執行緒安全:如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。
      如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
       若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;
      若有多個執行緒同時執行寫操作(更改變數),一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。
    • 執行緒同步:可理解為執行緒 A 和 執行緒 B 一塊配合,A 執行到一定程度時要依靠執行緒 B 的某個結果,於是停下來,示意 B 執行;B 依言執行,再將結果給 A;A 再繼續操作。


    例項“模擬火車票售賣”

    下面,我們模擬火車票售賣的方式,實現 NSOperation 執行緒安全和解決執行緒同步問題。 場景:總共有50張火車票,有兩個售賣火車票的視窗,一個是北京火車票售賣視窗,另一個是上海火車票售賣視窗。兩個視窗同時售賣火車票,賣完為止。


    /**
     * 非執行緒安全:不使用 NSLock
     * 初始化火車票數量、賣票視窗(非執行緒安全)、並開始賣票
     */
    - (void)initTicketStatusNotSave {
        NSLog(@"currentThread---%@",[NSThread currentThread]); // 列印當前執行緒
    
        self.ticketSurplusCount = 50;
    
        // 1.建立 queue1,queue1 代表北京火車票售賣視窗
        NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
        queue1.maxConcurrentOperationCount = 1;
    
        // 2.建立 queue2,queue2 代表上海火車票售賣視窗
        NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
        queue2.maxConcurrentOperationCount = 1;
    
        // 3.建立賣票操作 op1
        __weak typeof(self) weakSelf = self;
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            [weakSelf saleTicketNotSafe];
        }];
    
        // 4.建立賣票操作 op2
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            [weakSelf saleTicketNotSafe];
        }];
    
        // 5.新增操作,開始賣票
        [queue1 addOperation:op1];
        [queue2 addOperation:op2];
    }
    
    /**
     * 售賣火車票(非執行緒安全)
     */
    - (void)saleTicketNotSafe {
        while (1) {
    
            if (self.ticketSurplusCount > 0) {
                //如果還有票,繼續售賣
                self.ticketSurplusCount--;
                NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%d 視窗:%@", self.ticketSurplusCount, [NSThread currentThread]]);
                [NSThread sleepForTimeInterval:0.2];
            } else {
                NSLog(@"所有火車票均已售完");
                break;
            }
        }
    } 複製程式碼


    • 可以看到:在不考慮執行緒安全,不使用 NSLock 情況下,得到票數是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮執行緒安全問題。

    執行緒安全解決方案:可以給執行緒加鎖,
    在一個執行緒執行該操作的時候,不允許其他執行緒進行操作。

    iOS 實現執行緒加鎖有很多種方式。
    @synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各種方式。
    這裡我們使用 NSLock 物件來解決執行緒同步問題。
    NSLock 物件可以通過進入鎖時呼叫 lock 方法,解鎖時呼叫 unlock 方法來保證執行緒安全。


    /**
     * 執行緒安全:使用 NSLock 加鎖
     * 初始化火車票數量、賣票視窗(執行緒安全)、並開始賣票
     */
    
    - (void)initTicketStatusSave {
        NSLog(@"currentThread---%@",[NSThread currentThread]); // 列印當前執行緒
    
        self.ticketSurplusCount = 50;
    
        self.lock = [[NSLock alloc] init];  // 初始化 NSLock 物件
    
        // 1.建立 queue1,queue1 代表北京火車票售賣視窗
        NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
        queue1.maxConcurrentOperationCount = 1;
    
        // 2.建立 queue2,queue2 代表上海火車票售賣視窗
        NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
        queue2.maxConcurrentOperationCount = 1;
    
        // 3.建立賣票操作 op1
        __weak typeof(self) weakSelf = self;
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            [weakSelf saleTicketSafe];
        }];
    
        // 4.建立賣票操作 op2
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            [weakSelf saleTicketSafe];
        }];
    
        // 5.新增操作,開始賣票
        [queue1 addOperation:op1];
        [queue2 addOperation:op2];
    }
    
    /**
     * 售賣火車票(執行緒安全)
     */
    - (void)saleTicketSafe {
        while (1) {
    
            // 加鎖
            [self.lock lock];
    
            if (self.ticketSurplusCount > 0) {
                //如果還有票,繼續售賣
                self.ticketSurplusCount--;
                NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%d 視窗:%@", 
    self.ticketSurplusCount, [NSThread currentThread]]);
                [NSThread sleepForTimeInterval:0.2];
            }
    
            // 解鎖
            [self.lock unlock];
    
            if (self.ticketSurplusCount <= 0) {
                NSLog(@"所有火車票均已售完");
                break;
            }
        }
    } 複製程式碼
    • 可以看出:在考慮了執行緒安全,使用 NSLock 加鎖、解鎖機制的情況下,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了多個執行緒同步的問題。

    10.1 NSOperation 常用屬性和方法

    1. 取消操作方法
      • - (void)cancel; 可取消操作,實質是標記 isCancelled 狀態。
    2. 判斷操作狀態方法
      • - (BOOL)isFinished; 判斷操作是否已經結束。
      • - (BOOL)isCancelled; 判斷操作是否已經標記為取消。
      • - (BOOL)isExecuting; 判斷操作是否正在在執行。
      • - (BOOL)isReady; 判斷操作是否處於準備就緒狀態,這個值和操作的依賴關係相關。
    3. 操作同步
      • - (void)waitUntilFinished; 阻塞當前執行緒,直到該操作結束。可用於執行緒執行順序的同步。
      • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 會在當前操作執行完畢時執行 completionBlock。
      • - (void)addDependency:(NSOperation *)op; 新增依賴,使當前操作依賴於操作 op 的完成。
      • - (void)removeDependency:(NSOperation *)op; 移除依賴,取消當前操作對操作 op 的依賴。
      • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在當前操作開始執行之前完成執行的所有操作物件陣列。

    10.2 NSOperationQueue 常用屬性和方法

    1. 取消/暫停/恢復操作
      • - (void)cancelAllOperations; 可以取消佇列的所有操作。
      • - (BOOL)isSuspended; 判斷佇列是否處於暫停狀態。 YES 為暫停狀態,NO 為恢復狀態。
      • - (void)setSuspended:(BOOL)b; 可設定操作的暫停和恢復,YES 代表暫停佇列,NO 代表恢復佇列。
    2. 操作同步
      • - (void)waitUntilAllOperationsAreFinished; 阻塞當前執行緒,直到佇列中的操作全部執行完畢。
    3. 新增/獲取操作
      • - (void)addOperationWithBlock:(void (^)(void))block; 向佇列中新增一個 NSBlockOperation 型別操作物件。
      • - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向佇列中新增運算元組,wait 標誌是否阻塞當前執行緒直到所有操作結束
      • - (NSArray *)operations; 當前在佇列中的運算元組(某個操作執行結束後會自動從這個陣列清除)。
      • - (NSUInteger)operationCount; 當前佇列中的運算元。
    4. 獲取佇列
      • + (id)currentQueue; 獲取當前佇列,如果當前執行緒不是在 NSOperationQueue 上執行則返回 nil。
      • + (id)mainQueue; 獲取主佇列。

    注意:

    1. 這裡的暫停和取消(包括操作的取消和佇列的取消)並不代表可以將當前的操作立即取消,而是噹噹前的操作執行完畢之後不再執行新的操作。
    2. 暫停和取消的區別就在於:暫停操作之後還可以恢復操作,繼續向下執行;而取消操作之後,所有的操作就清空了,無法再接著執行剩下的操作。