iOS探索 多線程之NSOperation

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)git

寫在前面

GCD同樣,NSOperation也是咱們平常開發中常常用到的多線程技術。本文將會介紹NSOperation的基本使用、添加依賴、自定義github

1、初次使用

NSOperation是個抽象類,依賴於子類NSInvocationOperationNSBlockOperation去實現面試

下面是開發者文檔上對NSOperation的一段描述 編程

1.NSInvocationOperation

  • 基本使用
- (void)test {
    // 處理事務
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(handleInvocation:) object:@"Felix"];
    // 建立隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 操做加入隊列
    [queue addOperation:op];
}

- (void)handleInvocation:(id)operation {
    NSLog(@"%@ --- %@",op, [NSThread currentThread]);
}
--------------------輸出結果:-------------------
Felix --- <NSThread: 0x6000000422c0>{number = 3, name = (null)}
--------------------輸出結果:-------------------
複製代碼
  • 直接處理事務,不添加隱性隊列
- (void)test {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"Felix"];
    [op start];
}
複製代碼

接下來就會引伸出下面一段錯誤使用代碼緩存

- (void)test {
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation:) object:@"Felix"];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op];
    [op start];
}
--------------------錯誤日誌:-------------------
something is trying to start the receiver simultaneously from more than one thread'
--------------------錯誤日誌:-------------------
複製代碼

上述代碼之因此會崩潰,是由於線程生命週期:bash

  • queue addOperation:op已經將處理事務的操做任務加入到隊列中,並讓線程運行
  • op start將已經運行的線程再次運行會形成線程混亂

2.NSBlockOperation

NSInvocationOperationNSBlockOperation二者的區別在於:網絡

  • 前者相似target形式
  • 後者相似block形式——函數式編程,業務邏輯代碼可讀性更高
- (void)test {
    // 初始化添加事務
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務1————%@",[NSThread currentThread]);
    }];
    // 添加事務
    [bo addExecutionBlock:^{
        NSLog(@"任務2————%@",[NSThread currentThread]);
    }];
    // 回調監聽
    bo.completionBlock = ^{
        NSLog(@"完成了!!!");
    };
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo];
    NSLog(@"事務添加進了NSOperationQueue");
}

--------------------輸出結果:-------------------
事務添加進了NSOperationQueue
任務1————<NSThread: 0x6000032dc1c0>{number = 5, name = (null)}
任務2————<NSThread: 0x6000032a1880>{number = 4, name = (null)}
完成了!!!
--------------------輸出結果:-------------------
複製代碼

NSOperationQueue是異步執行的,因此任務一任務二的完成順序不肯定多線程

3.執行順序

下列代碼能夠證實操做與隊列的執行效果是異步併發併發

- (void)test {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for (int i = 0; i < 5; i++) {
        [queue addOperationWithBlock:^{
            NSLog(@"%@---%d", [NSThread currentThread], i);
        }];
    }
}
--------------------輸出結果:-------------------
<NSThread: 0x600002771600>{number = 3, name = (null)}---0
<NSThread: 0x60000277ac80>{number = 7, name = (null)}---3
<NSThread: 0x600002774840>{number = 6, name = (null)}---2
<NSThread: 0x600002776a80>{number = 8, name = (null)}---4
<NSThread: 0x60000270c540>{number = 5, name = (null)}---1
--------------------輸出結果:-------------------
複製代碼

4.設置優先級

- (void)test {
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            //sleep(1);
            NSLog(@"第一個操做 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 設置最高優先級
    bo1.qualityOfService = NSQualityOfServiceUserInteractive;
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"第二個操做 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 設置最低優先級
    bo2.qualityOfService = NSQualityOfServiceBackground;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo1];
    [queue addOperation:bo2];
}
複製代碼

NSOperation設置優先級只會讓CPU有更高的概率調用,不是說設置高就必定所有先完成異步

  • 不使用sleep——高優先級的任務一先於低優先級的任務二
第一個操做 0 --- <NSThread: 0x600002254280>{number = 6, name = (null)}
第一個操做 1 --- <NSThread: 0x600002254280>{number = 6, name = (null)}
第一個操做 2 --- <NSThread: 0x600002254280>{number = 6, name = (null)}
第一個操做 3 --- <NSThread: 0x600002254280>{number = 6, name = (null)}
第一個操做 4 --- <NSThread: 0x600002254280>{number = 6, name = (null)}
第二個操做 0 --- <NSThread: 0x600002240340>{number = 7, name = (null)}
第二個操做 1 --- <NSThread: 0x600002240340>{number = 7, name = (null)}
第二個操做 2 --- <NSThread: 0x600002240340>{number = 7, name = (null)}
第二個操做 3 --- <NSThread: 0x600002240340>{number = 7, name = (null)}
第二個操做 4 --- <NSThread: 0x600002240340>{number = 7, name = (null)}
複製代碼
  • 使用sleep進行延時——高優先級的任務一慢於低優先級的任務二
第二個操做 0 --- <NSThread: 0x600002b35840>{number = 7, name = (null)}
第二個操做 1 --- <NSThread: 0x600002b35840>{number = 7, name = (null)}
第二個操做 2 --- <NSThread: 0x600002b35840>{number = 7, name = (null)}
第二個操做 3 --- <NSThread: 0x600002b35840>{number = 7, name = (null)}
第二個操做 4 --- <NSThread: 0x600002b35840>{number = 7, name = (null)}
第一個操做 0 --- <NSThread: 0x600002b3c700>{number = 5, name = (null)}
第一個操做 1 --- <NSThread: 0x600002b3c700>{number = 5, name = (null)}
第一個操做 2 --- <NSThread: 0x600002b3c700>{number = 5, name = (null)}
第一個操做 3 --- <NSThread: 0x600002b3c700>{number = 5, name = (null)}
第一個操做 4 --- <NSThread: 0x600002b3c700>{number = 5, name = (null)}
複製代碼

5.線程間通信

  • GCD中使用異步進行網絡請求,而後回到主線程刷新UI
  • NSOperation中也有相似在線程間通信的操做
- (void)test {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"Felix";
    [queue addOperationWithBlock:^{
        NSLog(@"請求網絡%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        }];
    }];
}
--------------------輸出結果:-------------------
請求網絡<NSOperationQueue: 0x7ff4a240bae0>{name = 'Felix'}--<NSThread: 0x6000007dcf00>{number = 5, name = (null)}
刷新UI<NSOperationQueue: 0x7ff4a24087d0>{name = 'NSOperationQueue Main Queue'}--<NSThread: 0x60000078c8c0>{number = 1, name = main}
--------------------輸出結果:-------------------
複製代碼

6.設置併發數

  • GCD中只能使用信號量來設置併發數
  • NSOperation輕易就能設置併發數
    • 經過設置maxConcurrentOperationCount來控制單次出隊列去執行的任務數
- (void)test {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"Felix";
    queue.maxConcurrentOperationCount = 2;
    
    for (int i = 0; i < 5; i++) {
        [queue addOperationWithBlock:^{ // 一個任務
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        }];
    }
}
--------------------輸出結果:-------------------
1-<NSThread: 0x6000009290c0>{number = 5, name = (null)}
0-<NSThread: 0x6000009348c0>{number = 8, name = (null)}
3-<NSThread: 0x6000009290c0>{number = 5, name = (null)}
2-<NSThread: 0x60000094b0c0>{number = 7, name = (null)}
4-<NSThread: 0x6000009348c0>{number = 8, name = (null)}
--------------------輸出結果:-------------------
複製代碼

7.添加依賴

NSOperation中添加依賴能很好的控制任務執行的前後順序

- (void)test {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"請求token");
    }];
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"拿着token,請求數據1");
    }];
    
    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"拿着數據1,請求數據2");
    }];
    
    [bo2 addDependency:bo1];
    [bo3 addDependency:bo2];
    
    [self.queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
    
    NSLog(@"執行完了?我要幹其餘事");
}

--------------------輸出結果:-------------------
請求token
拿着token,請求數據1
拿着數據1,請求數據2
執行完了?我要幹其餘事
--------------------輸出結果:-------------------
複製代碼

注意不要添加依賴致使循環運用,會致使依賴無效並會在控制檯打印出"XPC connection interrupted"

8.任務的掛起、繼續、取消

// 掛起
queue.suspended = YES;
// 繼續
queue.suspended = NO;
// 取消
[queue cancelAllOperations];
複製代碼

可是使用中常常會遇到一些匪夷所思的問題——明明已經掛起了任務,可仍是繼續執行了幾個任務才中止執行

這幅圖是併發量爲2的狀況:

  • 掛起前:任務3任務4等待被調度
  • 掛起瞬間:任務3任務4已經被調度出隊列,準備執行,此時它們是沒法掛起的
  • 掛起後:任務3任務4被線程執行,而原來的隊列被掛起不能被調度

2、自定義NSOperation緩存機制

咱們平常開發中常常用SDWebImage去加載網絡圖片,其中又是什麼原理呢?若是要咱們本身來實現又該怎麼去作呢?

NSURL   *imageURL     = [NSURL URLWithString:model.imageUrl];
[cell.imageView sd_setImageWithURL:imageURL placeholderImage:[UIImage imageNamed:@"Felix"]];
return cell;
複製代碼

1.下載圖片

使用圖片地址去下載NSData數據,再轉成相應的UIImage圖片

cell.imageView.image  = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
複製代碼

Q1:主線程使用NSDataUIImage會形成卡頓,必需要解決這個問題

2.NSOperation異步處理

使用NSBlockOperation去異步處理數據,而後在主線程刷新UI,從而解決了卡頓的問題

NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"去下載圖片:%@", model.title);
    // 延遲
    NSData *data   = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];

    // 更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

[self.queue addOperation:bo];
return cell;
複製代碼

Q2:因爲cell的緩存機制,圖片每次都要去下載會形成浪費,因此要想辦法存起來

3.模型緩存

  • 若是模型中有數據,則從模型中取出圖片加載,節約內存消耗
  • 若是都沒有就異步下載把圖片數據存到模型
if (model.image) {
    NSLog(@"從模型獲取圖片:%@",model.title);
    cell.imageView.image = model.image;
    return cell;
}

NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"去下載圖片:%@", model.title);
    // 延遲
    NSData *data   = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];

    // 更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

[self.queue addOperation:bo];
return cell;
複製代碼

Q3:可是存到model裏會致使內存暴漲,此時只能清理model,但model中不僅有圖片數據,因此得另闢蹊徑處理緩存問題

4.內存緩存

  • 若是內存中有數據,則從內存中取出圖片加載,節約內存消耗
  • 若是都沒有就異步下載把圖片數據存到全局可變字典(內存)
UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
if (cacheImage) {
    NSLog(@"從內存獲取圖片:%@", model.title);
    cell.imageView.image = cacheImage;
    return cell;
}

NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"去下載圖片:%@", model.title);
    // 延遲
    NSData *data   = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];
    [self.imageCacheDict setValue:image forKey:model.imageUrl];

    // 更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

[self.queue addOperation:bo];
return cell;
複製代碼

Q4:可是內存會在App關閉時回收內存,致使每次重啓都要從新下載圖片

5.本地緩存

  • 第一次異步下載把圖片數據寫到本地緩存
  • 第二次加載圖片時直接能夠加載本地緩存中的數據,節約性能消耗
// 這裏對路徑進行了封裝處理,並進行了md5處理
UIImage *diskImage = [UIImage imageWithContentsOfFile:[model.imageUrl getDowloadImagePath]];
if (diskImage) {
    NSLog(@"從沙盒獲取圖片:%@",model.title);
    cell.imageView.image = diskImage;
    return cell;
}

NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"去下載圖片:%@", model.title);
    // 延遲
    NSData *data   = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];
    // 存內存
    [data writeToFile:[model.imageUrl getDowloadImagePath] atomically:YES];

    // 更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

[self.queue addOperation:bo];
return cell;
複製代碼

Q5:沙盒的效率沒有內存高,因此還得進行優化

6.本地緩存+內存緩存

  • 若是內存中有數據,則從內存中取出圖片來展現
  • 若是沙盒中有數據,則從沙盒中取出圖片來展現並存一份到內存中
  • 若是都沒有就異步下載把圖片數據寫到本地緩存內存緩存
UIImage *cacheImage = self.imageCacheDict[model.imageUrl];
if (cacheImage) {
    NSLog(@"從內存獲取圖片:%@", model.title);
    cell.imageView.image = cacheImage;
    return cell;
}

UIImage *diskImage = [UIImage imageWithContentsOfFile:[model.imageUrl getDowloadImagePath]];
if (diskImage) {
    NSLog(@"從沙盒獲取image:%@",model.title);
    cell.imageView.image = diskImage;
    [self.imageCacheDict setValue:diskImage forKey:model.imageUrl];
    return cell;
}

NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"去下載圖片:%@", model.title);
    // 延遲
    NSData *data   = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];
    // 存內存
    [self.imageCacheDict setValue:image forKey:model.imageUrl];
    [data writeToFile:[model.imageUrl getDowloadImagePath] atomically:YES];

    // 更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        cell.imageView.image = image;
    }];
}];

[self.queue addOperation:bo];
return cell;
複製代碼

這就是SDWebImage最簡易的步驟

寫在後面

筆者將文中內容封裝成一個Demo,有興趣能夠下載看看

相關文章
相關標籤/搜索