iOS開發(fā)多線程基礎講解四(NSOperation)

本文中所有代碼演示均有GitHub源碼,點擊下載

NSOperation 是iOS中比較高級的并發(fā)編程的操作!

NSOperation : 封裝了 GCD 中的"任務"!

NSOperationQueue : 封裝了 GCD 中的"隊列"!

  • NSOperation 是一個"抽象類",不能直接使用
  • 抽象類的用處是定義子類共有的屬性和方法
  • 在蘋果的頭文件中,有些抽象類和子類的定義是在同一個頭文件中的
  • 子類:
    • NSInvocationOperation (使用和按鈕/ target)
    • NSBlockOperation (利用block 封裝任務)
  • NSOperationQueue 隊列

其他基本的抽象類

  • UIGestureRecognizer
  • CAAnimation
  • CAPropertyAnimation

一. 基本演練

NSInvocationOperation

start

  • start 方法 會在當前線程執(zhí)行 @selector 方法
- (void)opDemo1 {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"Invocation"];

    // start方法:直接開始,會在當前線程執(zhí)行 @selector 方法
    [op start];
}

- (void)downloadImage:(id)obj {

    NSLog(@"%@ %@", [NSThread currentThread], obj);
}

添加到隊列

  • 將操作添加到隊列,會"異步"執(zhí)行 selector 方法
- (void)opDemo2 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

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

    [q addOperation:op];
}

添加多個操作

- (void)opDemo3 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@(i)];

        [q addOperation:op];
    }
}

執(zhí)行效果:會開啟多條線程,而且不是順序執(zhí)行。與GCD中并發(fā)隊列&異步執(zhí)行效果一樣!

結論,在 NSOperation 中:

  • 操作 -> 異步執(zhí)行的任務
  • 隊列 -> 全局隊列

NSBlockOperation

- (void)opDemo4 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];
    #warning 打印結果是在主線程中,說明直接在主線程執(zhí)行,沒有開辟新的線程
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
    }];

    [q addOperation:op];
}

使用 block 來定義操作,所有的代碼寫在一起,更簡單,便于維護!

更簡單的,直接添加 Block

- (void)opDemo5 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        [q addOperationWithBlock:^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];
    }
}

向隊列中添加不同的操作

- (void)opDemo5 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    for (int i = 0; i < 10; ++i) {
        [q addOperationWithBlock:^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];
    }

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block %@", [NSThread currentThread]);
    }];
    [q addOperation:op1];

    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"invocation"];
    [q addOperation:op2];
}
  • 可以向 NSOperationQueue 中添加任意 NSOperation 的子類

線程間通訊

- (void)opDemo6 {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];

    [q addOperationWithBlock:^{
        NSLog(@"耗時操作 %@", [NSThread currentThread]);

        // 主線程更新 UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"更新 UI %@", [NSThread currentThread]);
        }];
    }];
}

二. 高級演練

全局隊列

/// 全局操作隊列,統(tǒng)一管理所有的異步操作
@property (nonatomic, strong) NSOperationQueue *queue;

- (NSOperationQueue *)queue {
    if (_queue == nil) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

最大并發(fā)操作數(shù)

/// MARK: - 最大并發(fā)操作數(shù)
- (void)opDemo1 {

    // 設置同時并發(fā)操作數(shù)
    self.queue.maxConcurrentOperationCount = 2;

    NSLog(@"start");

    for (int i = 0; i < 10; ++i) {
        NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }];

        [self.queue addOperation:op];
    }
}

暫停 & 繼續(xù)

/// MARK: - 暫停 & 繼續(xù)
- (IBAction)pauseAndResume {

    if (self.queue.operationCount == 0) {
        NSLog(@"沒有操作");
        return;
    }

    // 暫?;蛘呃^續(xù)
    self.queue.suspended = !self.queue.isSuspended;

    if (self.queue.isSuspended) {
        NSLog(@"暫停 %tu", self.queue.operationCount);
    } else {
        NSLog(@"繼續(xù) %tu", self.queue.operationCount);
    }
}
  • 隊列掛起,當前"沒有完成的操作",是包含在隊列的操作數(shù)中的
  • 隊列掛起,不會影響已經(jīng)執(zhí)行操作的執(zhí)行狀態(tài)
  • 對列一旦被掛起,再添加的操作不會被調(diào)度

取消全部操作

/// MARK: - 取消所有操作
- (IBAction)cancelAll {
    if (self.queue.operationCount == 0) {
        NSLog(@"沒有操作");
        return;
    }

    // 取消對列中的所有操作,同樣不會影響到正在執(zhí)行中的操作!
    [self.queue cancelAllOperations];

    NSLog(@"取消全部操作 %tu", self.queue.operationCount);
}
  • 取消隊列中所有的操作
  • 不會取消正在執(zhí)行中的操作
  • 不會影響隊列的掛起狀態(tài)

依賴關系

#warning 不要添加循環(huán)操作依賴,一定要在添加進操作隊列之前設置操作依賴
- (void)dependency {

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"登錄 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"付費 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載 %@", [NSThread currentThread]);
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"通知用戶 %@", [NSThread currentThread]);
    }];

    [op2 addDependency:op1];
    [op3 addDependency:op2];
    [op4 addDependency:op3];
    // 注意不要循環(huán)依賴
//    [op1 addDependency:op4];

    [self.queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
    [[NSOperationQueue mainQueue] addOperation:op4];

    NSLog(@"come here");
}

追加操作

  • 當 NSBlockOperation 中的任務書 > 1 之后,無論是將操作添加到主線程,還是在主線程直接執(zhí)行start, NSBlockOperation中的任務執(zhí)行順序都不確定,執(zhí)行線程也不確定!

  • 一般在開發(fā)中,要避免向NSBlockOperation 中追加任務

  • 如果任務都是在主線程中執(zhí)行,并且不需要保證執(zhí)行順序,可以直接追加任務

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 實例化操作對象
    NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"11111--->%@", [NSThread currentThread]);
    }];

    // 2. 往當前操作中追加操作一
    [blockOperation1 addExecutionBlock:^{
        NSLog(@"追加任務1--->%@", [NSThread currentThread]);
    }];

    // 2. 往當前操作中追加操作二
    [blockOperation1 addExecutionBlock:^{
        NSLog(@"追加任務2--->%@", [NSThread currentThread]);
    }];

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


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

    // 將操作添加到主隊列中
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation1];
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation2];
    //    [[NSOperationQueue mainQueue] addOperation:blockOperation3];
    /*
     輸出結果:

     11111---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     追加任務2---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     追加任務1---><NSThread: 0x7fed62f07ba0>{number = 4, name = (null)}
     22222---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     33333---><NSThread: 0x7fed62e065f0>{number = 1, name = main}
     */


    // 將操作添加到非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [queue addOperation:blockOperation1];
    [queue addOperation:blockOperation2];
    [queue addOperation:blockOperation3];
    /*
     輸出結果:

     22222---><NSThread: 0x7fba18f08a20>{number = 9, name = (null)}
     11111---><NSThread: 0x7fba18f03780>{number = 7, name = (null)}
     33333---><NSThread: 0x7fba18d99c20>{number = 10, name = (null)}
     追加任務1---><NSThread: 0x7fba18f08770>{number = 8, name = (null)}
     追加任務2---><NSThread: 0x7fba18c06220>{number = 11, name = (null)}
     */

}

三. 與 GCD 的對比

  • GCD

    • 任務(block)添加到隊列(串行/并發(fā)/主隊列),并且指定任務執(zhí)行的函數(shù)(同步/異步)
    • GCD是底層的C語言構成的API
    • iOS 4.0 推出的,針對多核處理器的并發(fā)技術
    • 在隊列中執(zhí)行的是由 block 構成的任務,這是一個輕量級的數(shù)據(jù)結構
    • 要停止已經(jīng)加入 queueblock 需要寫復雜的代碼
    • 需要通過 Barrier 或者同步任務設置任務之間的依賴關系
    • 只能設置隊列的優(yōu)先級
    • 高級功能:
      • 一次性 once
      • 延遲操作 after
      • 調(diào)度組
  • NSOperation

    • 核心概念:把操作(異步)添加到隊列(全局的并發(fā)隊列)
    • OC 框架,更加面向?qū)ο?,是?GCD 的封裝
    • iOS 2.0 推出的,蘋果推出 GCD 之后,對 NSOperation 的底層全部重寫
    • Operation作為一個對象,為我們提供了更多的選擇
    • 可以隨時取消已經(jīng)設定要準備執(zhí)行的任務,已經(jīng)執(zhí)行的除外
    • 可以跨隊列設置操作的依賴關系
    • 可以設置隊列中每一個操作的優(yōu)先級
    • 高級功能:
      • 最大操作并發(fā)數(shù)(GCD不好做)
      • 繼續(xù)/暫停/全部取消
      • 跨隊列設置操作的依賴關系

四. 自定義操作

準備工作

  • 自定義 DownloadImageOperation 繼承自 NSOperation
  • 代碼調(diào)用
// 實例化自定義操作
DownloadImageOperation *op = [[DownloadImageOperation alloc] init];
// 將自定義操作添加到下載隊列
[self.downloadQueue addOperation:op];

需求驅(qū)動開發(fā)

目標一:設置自定義操作的執(zhí)行入口

對于自定義操作,只要重寫了 main 方法,當隊列調(diào)度操作執(zhí)行時,會自動運行 main 方法

注意:為了能夠及時釋放內(nèi)存,main方法內(nèi)部一般建立一個自動釋放池,但是蘋果官方文檔不要求寫!

- (void)main {
    @autoreleasepool {

        NSLog(@"main--->%@", [NSThread currentThread]);

        NSString *filePath = @"http://ww1.sinaimg.cn/bmiddle/c260f7abjw1exxbbyckd6j20gn0m8wgh.jpg";

        UIImage *image = [self downLoadImageWithStr:filePath];

        // 回到主線程設置UI
        [[NSOperationQueue mainQueue] addOperation:[NSBlockOperation blockOperationWithBlock:^{

            NSLog(@"setupUI--->%@", [NSThread currentThread]);

            self.imgView.image = image;
        }]];
    }
}

// 抽取出下載圖片的方法
- (UIImage *)downLoadImageWithStr:(NSString *)filePath {

    // 1. 轉(zhuǎn)化為地址
    NSURL *url = [NSURL URLWithString:filePath];

    // 2. 轉(zhuǎn)化為NSData 二進制類型數(shù)據(jù)(下載方法,耗時方法)
    NSData *data = [NSData dataWithContentsOfURL:url];

    // 3. 轉(zhuǎn)化為圖片類型
    UIImage *image = [UIImage imageWithData:data];

    NSLog(@"downLoadImage ---> %@", [NSThread currentThread]);

    return image;
}

目標二:給自定義參數(shù)傳遞參數(shù)

  • 定義屬性
/** 承載圖片的容器 */
@property (nonatomic, strong) UIImageView *imgView;
  • 代碼調(diào)用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSLog(@"touchBegin--->%@", [NSThread currentThread]);

    // 1. 實例化自定義操作對象
    NSDownLoadOperation *operation = [[NSDownLoadOperation alloc] init];

    // 2. 告訴操作在哪里顯示圖片
    // 注意: 不能做以下改變 self.imageView = [[UIImageView alloc] init];
    operation.imgView = self.imgView;

    // 3.1 將自定義操作添加到下載隊列,操作啟動后會執(zhí)行 main
     [self.queue addOperation:operation];

    // 3.2 直接開始
    // 這樣的話,全部操作都在當前線程(主線程)中進行操
    //[operation start];

    NSLog(@"touchEnd--->%@", [NSThread currentThread]);
}

注意,main 方法被調(diào)用時,屬性已經(jīng)準備就緒

目標三:如何回調(diào)?

利用系統(tǒng)提供的 CompletionBlock 屬性
// 設置完成回調(diào)
[operation setCompletionBlock:^{
    NSLog(@"完成 %@", [NSThread currentThread]);
}];
  • 只要設置了 CompletionBlock,當操作執(zhí)行完畢后,就會被自動調(diào)用
  • CompletionBlock 既不在主線程也不在操作執(zhí)行所在線程
  • CompletionBlock 無法傳遞參數(shù)

自己定義回調(diào) Block,在操作結束后執(zhí)行

  • block: 指向結構體的指針!塊代碼!閉包!!

  • 閉包: javascript/js最先使用 :可以從函數(shù)外部訪問函數(shù)內(nèi)部的變量! --- 靈活性!

    1. 定義 block 類型 (返回值、接受的參數(shù)類型...)
    2. 執(zhí)行 block 中執(zhí)行的內(nèi)容(block 中封裝的代碼)
    3. 執(zhí)行或調(diào)用 block (相當于調(diào)用函數(shù))

    ----以上三個步驟,必須按順序進行!
    可以在不同的對象中,分別設置三個步驟,只要保證順序就OK

  • 定義屬性

/// 完成回調(diào) Block
@property (nonatomic, copy) void (^finishedBlock)(UIImage *image);
  • 設置自定義回調(diào)
// 設置自定義完成回調(diào)
[op setFinishedBlock:^(UIImage *image) {
    NSLog(@"finished %@ %@", [NSThread currentThread], image);
}];
  • 耗時操作后執(zhí)行回調(diào)
// 判斷自定義回調(diào)是否存在
if (self.finishedBlock != nil) {
    // 通常為了簡化調(diào)用方的代碼,異步操作結束后的回調(diào),大多在主線程
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.finishedBlock(image對象);
    }];
}

目標四:簡化操作創(chuàng)建

  • 定義方法
///  實例化下載圖像操作
///
///  @param URLString 圖像 URL 字符串
///  @param finished  完成回調(diào) Block
///
///  @return 下載操作實例
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *image))finished;
  • 實現(xiàn)方法
+ (instancetype)downloadImageOperationWithURLString:(NSString *)URLString finished:(void (^)(UIImage *))finished {
    DownloadImageOperation *operation = [[DownloadImageOperation alloc] init];

    operation.URLString = URLString;
    operation.finishedBlock = finished;

    return operation;
}
  • 方法調(diào)用
// 使用類方法實例化下載操作
DownloadImageOperation *operation = [DownloadImageOperation downloadImageOperationWithURLString:@"http://www.baidu.com/img/bdlogo.png" finished:^(UIImage *image) {
    NSLog(@"%@", image);
}];

// 將自定義操作添加到下載隊列,操作啟動后會執(zhí)行 main 方法
[self.downloadQueue addOperation:op];

目標五:取消操作

在關鍵節(jié)點添加 isCancelled 判斷

  • 添加多個下載操作
for (int i = 0; i < 10; ++i) {
    NSString *urlString = [NSString stringWithFormat:@"http://www.xxx.com/%04d.png", i];

    DownloadImageOperation *op = [DownloadImageOperation downloadImageOperationWithURLString:urlString finished:^(UIImage *image) {
        NSLog(@"===> %@", image);
    }];

    // 將自定義操作添加到下載隊列,操作啟動后會執(zhí)行 main 方法
    [self.downloadQueue addOperation:op];
}
  • 設置隊列最大并發(fā)操作數(shù)
_downloadQueue.maxConcurrentOperationCount = 2;
  • 內(nèi)存警告時取消所有操作
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

    [self.downloadQueue cancelAllOperations];
}

cancelAllOperations 會向隊列中的所有操作發(fā)送 Cancel 消息

  • 調(diào)整 main 方法,在關鍵節(jié)點判斷
- (void)main {
    NSLog(@"%s", __FUNCTION__);

    @autoreleasepool {

        NSLog(@"下載圖像 %@", self.URLString);
        // 模擬延時
        [NSThread sleepForTimeInterval:1.0];

        if (self.isCancelled) {
            NSLog(@"1.--- 返回");
            return;
        }

        // 判斷自定義回調(diào)是否存在
        if (self.finishedBlock != nil) {
            // 通常為了簡化調(diào)用方的代碼,異步操作結束后的回調(diào),大多在主線程
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                self.finishedBlock(self.URLString);
            }];
        }
    }
}

注意:如果操作狀態(tài)已經(jīng)是 Cancel,則不會執(zhí)行 main 函數(shù)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容