iOS多線程之--NSOperation



iOS多線程demohtml

iOS多線程之--NSThreadc++

iOS多線程之--GCD詳解git

iOS多線程之--NSOperationgithub

iOS多線程之--線程安全(線程鎖)面試

iOS多線程相關面試題安全



1.NSOperation簡介

NSOperation是基於GCD更高一層的封裝,是面向對象的。相比GCDNSOperation的使用更加簡單,而且提供了一些用GCD不是很好實現的功能(好比設置最大併發數、隊列的暫停和繼續、取消任務、指定任務的依賴關係等)。bash

NSOperation是一個抽象類(c++中用virtual修飾的函數爲虛函數,虛函數是有方法實現的,虛函數的做用就是容許其在子類中能夠被重寫,因此OC的方法其實都是虛函數。虛函數後面加上=0就是純虛函數,好比virtual void add(int a,int b) = 0;就是純虛函數,純虛函數是沒有函數實現的,只能在子類中去實現。一個類只要有純虛函數那它就是一個抽象類,抽象類不能用來實例化對象,只能做爲基類用來繼承並在子類中重寫虛函數後才能使用。其實OC中是沒有抽象類的,但能夠結合OC的協議來實現抽象類。),也就是說它並不能直接使用,而是應該使用它的子類。使用它的子類的方法有三種,使用蘋果爲咱們提供的兩個子類 NSInvocationOperation,NSBlockOperation和自定義繼承自NSOperation的子類。網絡

2.NSInvocationOperation的使用

NSInvocationOperation直接使用時默認同步執行,也就是不會開啓新的線程去執行任務,而是在當前線程執行(以下所示,test函數是在哪一個線程執行,那麼task也就是在哪一個線程執行)。多線程

- (void)test{

    NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
    
    [io start];
}

- (void)task{
    NSLog(@"-->%@",[NSThread currentThread]);
}

// 打印結果
2019-11-27 10:19:51.304818+0800 myTest[10234:4208794] --><NSThread: 0x1007035d0>{number = 1, name = main}
複製代碼

3.NSBlockOperation的使用

若是隻有一個任務(或者叫操做),也就是經過blockOperationWithBlock添加的執行任務(或者經過init方法建立NSBlockOperation對象而後經過blockOperationWithBlock添加一個任務),那不會開啓新線程,而是在當前線程執行任務。併發

- (void)test{

    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1-->%@",[NSThread currentThread]);
    }];

    [bo start];
}

// 打印結果
2019-11-27 11:02:47.763536+0800 myTest[10825:4360047] 1--><NSThread: 0x10050b240>{number = 1, name = main}
複製代碼

若是經過addExecutionBlock額外添加了多個執行任務,那麼會開啓新線程去執行任務,此時blockOperationWithBlock添加的任務也不必定是在當前線程中執行。全部任務完成後執行completionBlock中的代碼,注意completionBlock要寫在start方法的前面。

- (void)test{

    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1-->%@",[NSThread currentThread]);
    }];
    
    [bo addExecutionBlock:^{
        NSLog(@"2-->%@",[NSThread currentThread]);
    }];
    
    [bo addExecutionBlock:^{
        NSLog(@"3-->%@",[NSThread currentThread]);
    }];
    
    [bo addExecutionBlock:^{
        NSLog(@"4-->%@",[NSThread currentThread]);
    }];
    
    bo.completionBlock = ^{
        NSLog(@"完成");
    };

    [bo start];
}

// 打印結果
2019-11-27 11:09:45.522125+0800 myTest[10884:4381034] 4--><NSThread: 0x1018071f0>{number = 4, name = (null)}
2019-11-27 11:09:45.522129+0800 myTest[10884:4381032] 1--><NSThread: 0x101800050>{number = 2, name = (null)}
2019-11-27 11:09:45.522114+0800 myTest[10884:4381033] 3--><NSThread: 0x1018000b0>{number = 3, name = (null)}
2019-11-27 11:09:45.522115+0800 myTest[10884:4380754] 2--><NSThread: 0x10050b240>{number = 1, name = main}
2019-11-27 11:09:45.522120+0800 myTest[10884:4380754] 完成
複製代碼

4.自定義繼承自 NSOperation 的子類

咱們還能夠自定義繼承自 NSOperation 的子類,重寫 main 方法 ,將要執行的任務放在main方法中,當調用start方法時會自動執行main方法中的代碼。

// 自定義子類的.h文件
#import <UIKit/UIKit.h>

@interface QJOperation : NSOperation

@end

// 自定義子類的.m文件
#import "QJOperation.h"

@implementation QJOperation

- (void)main
{
// 要執行的任務
        for (NSInteger i = 0; i < 3; i++) {
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"-->%@",[NSThread currentThread]);
    }
}

@end
複製代碼
// 使用 QJOperation
- (void)useCustomOperation {
    QJOperation *co = [[QJOperation alloc] init];
    [co start];
}
複製代碼

5.NSOperationQueue

5.1 主隊列

經過[NSOperationQueue mainQueue]獲取主隊列,通常添加到主隊列中的任務是在主線程中執行,可是經過NSBlockOperation添加的任務數若是大於1,那麼是會開啓新的線程去執行任務的。

NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSInvocationOperation *io = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
    
NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2-->%@",[NSThread currentThread]);
    }];
        
[bo addExecutionBlock:^{
        NSLog(@"3-->%@",[NSThread currentThread]);
    }];
    
[queue addOperation:io];
[queue addOperation:bo];

- (void)task{
  NSLog(@"1-->%@",[NSThread currentThread]);
}

// 打印結果
2019-11-27 16:00:44.342231+0800 AppTest[9485:3654469] 1--><NSThread: 0x282b4a000>{number = 1, name = main}
2019-11-27 16:00:44.344664+0800 AppTest[9485:3654502] 3--><NSThread: 0x282bcc340>{number = 5, name = (null)}
2019-11-27 16:00:44.344674+0800 AppTest[9485:3654469] 2--><NSThread: 0x282b4a000>{number = 1, name = main}
複製代碼

5.2 自定義隊列

經過[[NSOperationQueue alloc] init]建立自定義隊列,添加到自定義隊列中的任務會自動開啓子線程去執行(子線程的建立由系統控制,添加的多個任務可能在同一個子線程中執行也可能在不一樣子線程中執行)。自定義隊列和前面主隊列的使用方法同樣。

5.3 向隊列中添加任務

- (void)addOperation:(NSOperation *)op;添加單個任務(操做)。

- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait);添加多個任務,wait爲YES的話會阻塞線程,全部添加的任務都執行完後纔會繼續執行後面的代碼,爲NO的話會先執行後面的代碼再執行添加到隊列的任務。

- (void)addOperationWithBlock:(void (^)(void))block);以block的形式添加任務。

5.4 maxConcurrentOperationCount 控制串行、併發執行

能夠設置隊列的maxConcurrentOperationCount屬性來設置最大併發數(這裏注意最大併發數是指能併發執行的最大操做數,這並不等同於最大開啓的線程數,能夠經過下面代碼進行驗證)。其默認值是-1,表示最大併發數沒有限制(固然也不會超過系統設定的默認最大值),若是設置爲1的話就表示一次只能調度執行一個任務,也就是串行執行任務,不過這裏的串行和GCD的串行有點不同,GCD的串行是先添加的就先調度執行(也就是遵循FIFO原則),但這裏的串行執行順序和任務的優先級、依賴等因素有關,優先級高的任務後添加進隊列也可能會先調度執行。

// 設置最大併發數爲2,但打印顯示線程數並不必定是2
- (void)test{

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    queue.maxConcurrentOperationCount = 2;
    
    for (NSInteger i = 0; i < 5; i++) {
        NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"%ld-->%@",i,[NSThread currentThread]);
        }];
        [queue addOperation:bo];
    }
}

// 打印結果
2019-11-27 17:08:59.642005+0800 AppTest[9693:3678978] 0--><NSThread: 0x28132c680>{number = 5, name = (null)}
2019-11-27 17:08:59.642002+0800 AppTest[9693:3678982] 1--><NSThread: 0x28132c800>{number = 4, name = (null)}
2019-11-27 17:08:59.842901+0800 AppTest[9693:3678979] 2--><NSThread: 0x28131c840>{number = 6, name = (null)}
2019-11-27 17:08:59.842904+0800 AppTest[9693:3678978] 3--><NSThread: 0x28132c680>{number = 5, name = (null)}
2019-11-27 17:09:00.048630+0800 AppTest[9693:3678979] 4--><NSThread: 0x28131c840>{number = 6, name = (null)}
複製代碼

5.5 隊列的掛起與取消

但隊列中還有未調度的任務時,將隊列的suspended設置爲YES表示暫停調度,再次設置爲NO表示繼續調度任務,cancelAllOperations能夠取消全部爲調度的任務。好比一個頁面有不少張圖片要下載,當咱們退出這個頁面時,還未下載的圖片就不須要下載了,咱們就能夠將已經添加到隊列中的還未執行的下載任務給取消掉。若是想要知道隊列中的任務是否執行完了,能夠經過KVO監聽隊列的operationCount屬性值,爲0時表示所有執行完了。

這裏要注意,不論是掛起仍是取消,都是對隊列進行操做,只對隊列中還未執行(也就是正在等待調度)的任務有效,若是是已經調度出來正在運行的任務是沒法掛起和取消的。

// 暫停/繼續
- (void)suspendOperations{
    self.queue.suspended = !self.queue.isSuspended;
    if (self.queue.suspended) {
      NSLog(@"暫停");
    }else{
      NSLog(@"繼續");
    }
}

// 取消全部操做
- (void)cancelAllOperations{
    [self.queue cancelAllOperations];
    NSLog(@"取消全部操做");
}
複製代碼

5.6 線程通訊

開發中一般將耗時操做(好比網絡請求、下載等)放在子線程中執行,耗時操做完成時須要回到主線程刷新UI,這就須要用到線程通訊。

- (void)test{

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"開始子線程任務--%@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:1]; // 模擬耗時操做
        NSLog(@"結束子線程任務--%@",[NSThread currentThread]);
        
        // 回到主線程刷新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
           NSLog(@"回到主線程刷新UI--%@",[NSThread currentThread]);
        }];
    }];
    [queue addOperation:bo];
}

// 打印結果
2019-11-28 09:43:24.313447+0800 AppTest[10330:3871233] 開始子線程任務--<NSThread: 0x28179cec0>{number = 4, name = (null)}
2019-11-28 09:43:25.318741+0800 AppTest[10330:3871233] 結束子線程任務--<NSThread: 0x28179cec0>{number = 4, name = (null)}
2019-11-28 09:43:25.319329+0800 AppTest[10330:3871202] 回到主線程刷新UI--<NSThread: 0x2817fe000>{number = 1, name = main}
複製代碼

6. 操做的優先級與依賴

6.1 優先級

一個任務(操做)的默認優先級爲NSOperationQueuePriorityNormal,優先級高的任務只是說被調度的概率更大,並不能保證必定先執行。即使是最大併發數爲1時,後添加進隊列的任務若是優先級高也可能會先調度執行(注意先調度並不表明先執行完,這和任務要執行的時長也有關係)。另外只有在同一個隊列中的任務設置不一樣優先級纔會起做用,也就是說不一樣的隊列中的任務的優先級是相互獨立的。

// 操做的優先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
	NSOperationQueuePriorityVeryLow = -8L,
	NSOperationQueuePriorityLow = -4L,
	NSOperationQueuePriorityNormal = 0, // 默認優先級
	NSOperationQueuePriorityHigh = 4,
	NSOperationQueuePriorityVeryHigh = 8
};
複製代碼
複製代碼

6.2 操做的依賴關係

項目中常常會遇到一個任務依賴另外一個任務的需求,好比一個APP須要先登陸成功後才能進行其餘網絡請求操做,也就是說其餘網絡請求操做是依賴登陸操做的。這裏就能夠經過添加操做的依賴關係來實現這樣的需求,可是添加依賴時要注意不要出現相互依賴的狀況,好比a依賴b,b依賴c,c又依賴a就會出問題。另外有依賴關係的2個任務能夠不在一個隊列中

添加依賴後就是按照依賴的順序來執行,此時任務的優先級就不起做用了。當一個任務添加依賴後,在它說依賴的任務執行完以前,它的狀態是未就緒的狀態(也就是isReady屬性爲NO),全部依賴都執行完後isaReady才變爲YES,此時這個任務才能被調度執行。

添加多個依賴也是能夠的,好比若是bo1任務是下載圖片a,bo2任務是下載圖片b,bo3的任務是拼接圖片a和b,那麼bo1和bo2是沒有依賴關係的,可是bo3要同時依賴bo1和bo2([bo3 addDependency:bo1];[bo3 addDependency:bo2];),那麼bo1和bo2都執行完成後才執行bo3。

- (void)test{

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務1");
    }];
    bo1.queuePriority = NSOperationQueuePriorityVeryHigh;
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務2");
    }];
    
    bo2.queuePriority = NSOperationQueuePriorityNormal;
    
    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務3");
    }];
    
    bo3.queuePriority = NSOperationQueuePriorityVeryLow;
    
    // 添加依賴
    [bo1 addDependency:bo2]; // bo1依賴bo2,也就是bo2執行完了纔會執行bo1
    [bo2 addDependency:bo3]; // bo2依賴bo3,也就是bo3執行完了纔會執行bo2
    
    [queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:NO];
    
    // 回到主線程刷新UI
    NSBlockOperation *bo4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"回到主線程刷新UI-->%@",[NSThread currentThread]);
    }];
    // 有依賴關係的2個任務能夠不在一個隊列中
    [bo4 addDependency:bo1];
    [[NSOperationQueue mainQueue] addOperation:bo4];
}

// 打印結果(執行順序始終是3,2,1)
2019-11-27 17:58:23.749955+0800 AppTest[9748:3692966] 任務3
2019-11-27 17:58:24.755548+0800 AppTest[9748:3692969] 任務2
2019-11-27 17:58:25.761256+0800 AppTest[9748:3692968] 任務1
2019-11-27 17:58:25.870941+0800 AppTest[10277:3859398] 回到主線程刷新UI--><NSThread: 0x28291df80>{number = 1, name = main}
複製代碼

參考資料:NSOperation的進階使用和簡單探討

參考資料:iOS 多線程:『NSOperation、NSOperationQueue』詳盡總結

相關文章
相關標籤/搜索