NSOperation的多執行緒使用以及和GCD的對比

Deft_MKJing宓珂璟發表於2016-12-23

NSOperation介紹

基本使用介紹
NSOperation類是一個抽象類,用於封裝與單個任務相關聯的程式碼和資料。 因為它是抽象的,你不直接使用這個類,而是子類或使用系統定義的子類之一(NSInvocationOperation或NSBlockOperation)來執行實際任務。 儘管是抽象的,NSOperation的基本實現確實包括重要的邏輯來協調您的任務的安全執行。 這種內建邏輯的存在允許您專注於任務的實際實現,而不是在確保其與其他系統物件正確工作所需的粘合程式碼上。

GCD佇列型別

  • 併發佇列
    • 自己建立的
    • 全域性
  • 序列佇列
    • 主佇列
    • 自己建立

NSOperationQueue的佇列型別

  • 主佇列
    • [NSOperationQueue mainQueue]
    • 凡是新增到主佇列的任務(NSOperation),都會放到主執行緒中執行
  • 非主佇列(其他佇列)
    • [[NSOperationQueue alloc] init]
    • 同時包含了序列併發功能
    • 新增到這個佇列的任務(NSOperation)自定會放到子執行緒執行

基本用法用法介紹

// 通過佇列的方式建立
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
       NSLog(@"任務5,%@",[NSThread currentThread]);
    }];

    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test1) object:nil];

    // 自定義
    MKJOperation *op3 = [[MKJOperation alloc] init];
    // 重寫main函式即可
//    - (void)main
//    {
//        NSLog(@"我是自定義的任務%@,%d",[NSThread currentThread],[self isConcurrent]);
//    }

    // 預設實在子執行緒  而且不需要呼叫start方法
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
//    2016-12-23 08:51:06.400 NSOperation[1439:67271] 任務1<NSThread: 0x608000265ec0>{number = 3, name = (null)}
//    2016-12-23 08:51:06.400 NSOperation[1439:67274] 任務5,<NSThread: 0x600000263d40>{number = 5, name = (null)}
//    2016-12-23 08:51:06.400 NSOperation[1439:67272] 我是自定義的任務<NSThread: 0x600000262ec0>{number = 4, name = (null)}



需要注意的是對於自定義Operation的實現部分,首先我們已經知道GCD裡面有非同步序列和非同步併發,那麼NSOperation是GCD的封裝,自然也可以,通過暴露給外部的 isConcurrent來檢測是哪一個。

文件這麼說:
The value of this property is YES for operations that run asynchronously with respect to the current thread or NO for operations that run synchronously on the current thread. The default value of this property is NO.

也就是預設是NO,非同步序列的,文件對自定義NSOperation重寫有兩種,就是根據這個型別來的,預設的時候是NO,非同步序列,那麼只要實現main一個函式即可
在這個方法中,你放置執行給定任務所需的程式碼。 當然,您還應該定義一個自定義初始化方法,以便更容易建立自定義類的例項。 您可能還需要定義getter和setter方法以從操作訪問資料。 但是,如果您定義了自定義的getter和setter方法,您必須確保這些方法可以從多個執行緒安全地呼叫。

屬性之maxConcurrentOperationCount

預設是-1 由系統控制
NSOperationQueueDefaultMaxConcurrentOperationCount = -1;

加到NSOperationQueue的任務預設都是非同步併發執行的,當設定該屬性大於1的時候就是,如果你要實現序列任務執行,queue.maxConcurrentOperationCount = 1; 一句話搞定

屬性之suspended

// 通過佇列的方式建立
    self.queue = [[NSOperationQueue alloc] init];
    self.queue.maxConcurrentOperationCount = 1;
    [self.queue addOperationWithBlock:^{
        for (NSInteger i =0; i < 10000; i ++) {
            NSLog(@"任務1%@,%ld",[NSThread currentThread],i);
        }
    }];

    [self.queue addOperationWithBlock:^{
        for (NSInteger i =0; i < 1000; i ++) {
            NSLog(@"任務2%@",[NSThread currentThread]);
        }
    }];

    [self.queue addOperationWithBlock:^{
        for (NSInteger i =0; i < 1000; i ++) {
            NSLog(@"任務3%@",[NSThread currentThread]);
        }
    }];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.queue.suspended = !self.queue.suspended;
}



這裡需要注意的是,呼叫suspended屬性的時候為YES的時候,前面的執行10000次列印不是中途停止(例如列印到5550次停止)該任務還是會繼續列印完10000次,而是任務2和任務3被掛起,不在執行,當再一次被點選的時候,會繼續列印之後的操作,根據這個特性可以優化tableview,當滑動的時候把所有任務都掛起,優化使用者體驗,滑動結束的時候繼續執行suspended = NO即可。

方法之cancelAllOperations

和上面的suspended一樣,呼叫這個方法的時候,還是會繼續執行完當前任務,取消掉後面的所有任務。如果顯示地呼叫類似各種系統自帶的Operation加入到佇列裡面去,呼叫cancel方法是沒有問題,但是當你自定義的時候,例如你有好幾個耗時操作,你都重寫了main 方法,在裡面進行coding,這個時候你在外面呼叫cancel的時候是不會取消裡面的任務的,你可以自己試試,他會把被這個Operation包裹起來的所有任務執行完,取消的是之後的Operation,如果你要取消自定義任務裡面的單個耗時操作,你只需要這麼做就好了

- (void)main
{
    // 耗時操作1
    for (NSInteger i =0; i < 5000; i ++) {
        NSLog(@"任務1%@,%ld",[NSThread currentThread],i);
    }
    // 如果不進行判斷是無法進行取消的,還是會全部執行
    if (self.isCancelled) {
        return;
    }
    // 耗時任務2
    for (NSInteger i =0; i < 1000; i ++) {
        NSLog(@"任務2%@,%ld",[NSThread currentThread],i);
    }
    if (self.isCancelled) {
        return;
    }
    // 耗時任務3
    for (NSInteger i =0; i < 1000; i ++) {
        NSLog(@"任務3%@,%ld",[NSThread currentThread],i);
    }
}

為什麼這麼做,官方介紹如下
特別是,你的主要任務程式碼應該定期檢查被取消的屬性的值。 如果屬性報告值YES,您的操作物件應儘快清除並退出。
You should always support cancellation semantics in any custom code you write. In particular, your main task code should periodically check the value of the cancelled property. If the property reports the value YES, your operation object should clean up and exit as quickly as possible. If you implement a custom start method, that method should include early checks for cancellation and behave appropriately. Your custom start method must be prepared to handle this type of early cancellation.

方法之addDependency任務依賴

    // 通過佇列的方式建立
    self.queue = [[NSOperationQueue alloc] init];

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務1,%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務2,%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(0.02);
        NSLog(@"任務3,%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務4,%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務5,%@",[NSThread currentThread]);
    }];
    // 4必定在1 2 3都執行完之後才能執行,而1,2,3,5還是無序的
    [op4 addDependency:op1];
    [op4 addDependency:op2];
    [op4 addDependency:op3];

    [self.queue addOperation:op1];
    [self.queue addOperation:op2];
    [self.queue addOperation:op3];
    [self.queue addOperation:op4];
    [self.queue addOperation:op5];

    // 未設定依賴
//    2016-12-23 10:37:00.346 NSOperation[3184:204706] 任務4,<NSThread: 0x600000071480>{number = 6, name = (null)}
//    2016-12-23 10:37:00.346 NSOperation[3184:204704] 任務2,<NSThread: 0x608000066c40>{number = 4, name = (null)}
//    2016-12-23 10:37:00.346 NSOperation[3184:204703] 任務3,<NSThread: 0x600000071440>{number = 3, name = (null)}
//    2016-12-23 10:37:00.346 NSOperation[3184:204717] 任務1,<NSThread: 0x608000073d40>{number = 5, name = (null)}
//    2016-12-23 10:37:00.347 NSOperation[3184:204704] 任務5,<NSThread: 0x608000066c40>{number = 4, name = (null)}

    // 設定依賴

//    2016-12-23 10:39:38.817 NSOperation[3266:209106] 任務2,<NSThread: 0x6000002690c0>{number = 4, name = (null)}
//    2016-12-23 10:39:38.817 NSOperation[3266:209105] 任務1,<NSThread: 0x60800007fb00>{number = 3, name = (null)}
//    2016-12-23 10:39:38.817 NSOperation[3266:209108] 任務3,<NSThread: 0x60800026a240>{number = 5, name = (null)}
//    2016-12-23 10:39:38.817 NSOperation[3266:209124] 任務5,<NSThread: 0x60800026af80>{number = 6, name = (null)}
//    2016-12-23 10:39:38.819 NSOperation[3266:209108] 任務4,<NSThread: 0x60800026a240>{number = 5, name = (null)}

牛逼之處在於可以不同佇列進行任務相互依賴
多佇列任務貫穿依賴

屬性之comoletionBlock

@property (nullable, copy) void (^completionBlock)(void)

op4.completionBlock = ^{
        NSLog(@"任務4完成了");
    };

使用場景:多圖片下載合成

self.queue = [[NSOperationQueue alloc] init];
    // 任務1
    NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://sc4.hao123img.com/data/2016-09-07/1_e5b664a087a52159bf21afa364f5e285_0"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        weakSelf.image1 = [UIImage imageWithData:data];
    }];

    // 任務2
    NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
        // 這裡可以一樣是耗時的網路請求,暫時處理成本地的
        weakSelf.image2 = [UIImage imageNamed:@"Play"];
    }];

    // 合成任務
    NSBlockOperation *combine = [NSBlockOperation blockOperationWithBlock:^{
        UIGraphicsBeginImageContext(CGSizeMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width));
        [weakSelf.image1 drawInRect:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width)];
        [weakSelf.image2 drawInRect:CGRectMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.width/2, 30, 40)];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            weakSelf.imageView.image = image;
        }];

    }];
    // 設定依賴
    [combine addDependency:download1];
    [combine addDependency:download2];
    // 加入佇列
    [self.queue addOperation:download1];
    [self.queue addOperation:download2];
    [self.queue addOperation:combine];

這裡寫圖片描述


GCD實現該方法需要用到Group,但是NSOperation十分簡單就能實現依賴,操作起來更加物件導向

GCD和NSOperation的區別

  • GCD是底層的C語言構成的API,而NSOperationQueue及相關物件是Objc的物件。
    在GCD中,在佇列中執行的是由block構成的任務,這是一個輕量級的資料結構;
    而Operation作為一個物件,為我們提供了更多的選擇;
    GCD面向C,NSOperation是GCD的封裝,物件導向

  • 在NSOperationQueue中,我們可以隨時取消已經設定要準備執行的任務(當然,
    已經開始的任務就無法阻止了),而GCD沒法停止已經加入queue的block(其實是有的,
    但需要許多複雜的程式碼);
    NSOperation可以取消任務,GCD不行

  • NSOperation能夠方便地設定依賴關係,我們可以讓一個Operation依賴於另一個Operation,
    這樣的話儘管兩個Operation處於同一個並行佇列中,但前者會直到後者執行完畢後再執行;
    超級強大的依賴關係,而且能設定不同佇列之之間的任務依賴

  • 我們能將KVO應用在NSOperation中,可以監聽一個Operation是否完成或取消,
    這樣子能比GCD更加有效地掌控我們執行的後臺任務;
    NSOperation用KVO監聽完成,取消,開始,掛起等狀態,比GCD更能掌控後臺操作

  • 在NSOperation中,我們能夠設定NSOperation的priority優先順序,
    能夠使同一個並行佇列中的任務區分先後地執行,而在GCD中,我們只能區分不同任務佇列的優先順序,
    如果要區分block任務的優先順序,也需要大量的複雜程式碼;
    NSOperation可以設定任務之間的優先順序,GCD只能設定不同佇列之間的優先順序,如果要做,需要大量程式碼

  • 我們能夠對NSOperation進行繼承,在這之上新增成員變數與成員方法,
    提高整個程式碼的複用度,這比簡單地將block任務排入執行佇列更有自由度,
    能夠在其之上新增更多自定製的功能。
    NSOperation抽象類,我們能自定義子類進行重寫,更具自由度,而且有可複用性

    Operation Queues :相對 GCD 來說,使用 Operation Queues 會增加一點點額外的開銷,但是我們卻換來了非常強大的靈活性和功能,我們可以給 operation 之間新增依賴關係、取消一個正在執行的 operation 、暫停和恢復 operation queue 等;
    GCD :則是一種更輕量級的,以 FIFO 的順序執行併發任務的方式,使用 GCD 時我們並不關心任務的排程情況,而讓系統幫我們自動處理。但是 GCD 的短板也是非常明顯的,比如我們想要給任務之間新增依賴關係、取消或者暫停一個正在執行的任務時就會變得非常棘手

GCD用法和介紹傳送門

runtime面試問題以及常用用法

相關文章