鍍金池/ 教程/ iOS/ 常見的后臺實踐
與四軸無人機的通訊
在沙盒中編寫腳本
結構體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術:Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉字符串
相機工作原理
Build 過程

常見的后臺實踐

本文主要探討一些常用后臺任務的最佳實踐。我們將會看看如何并發(fā)地使用 Core Data ,如何并行繪制 UI ,如何做異步網(wǎng)絡請求等。最后我們將研究如何異步處理大型文件,以保持較低的內(nèi)存占用。因為在異步編程中非常容易犯錯誤,所以,本文中的例子都將使用很簡單的方式。因為使用簡單的結構可以幫助我們看透代碼,抓住問題本質(zhì)。如果你最后把代碼寫成了復雜的嵌套回調(diào)的話,那么你很可能應該重新考慮自己當初的設計選擇了。

操作隊列 (Operation Queues) 還是 GCD ?

目前在 iOS 和 OS X 中有兩套先進的同步 API 可供我們使用:操作隊列GCD 。其中 GCD 是基于 C 的底層的 API ,而操作隊列則是 GCD 實現(xiàn)的 Objective-C API。關于我們可以使用的并行 API 的更加全面的總覽,可以參見 并發(fā)編程:API 及挑戰(zhàn)。

操作隊列提供了在 GCD 中不那么容易復制的有用特性。其中最重要的一個就是可以取消在任務處理隊列中的任務,在稍后的例子中我們會看到這個。而且操作隊列在管理操作間的依賴關系方面也容易一些。另一面,GCD 給予你更多的控制權力以及操作隊列中所不能使用的底層函數(shù)。詳細介紹可以參考底層并發(fā) API 這篇文章。

擴展閱讀:

March 2015 更新: 這篇文章是基于已經(jīng)過時了的 Concurrency with Core Data 來編寫的。

后臺的 Core Data

在著手 Core Data 的并行處理之前,最好先打一些基礎。我們強烈建議通讀蘋果的官方文檔 Concurrency with Core Data 。這個文檔中羅列了基本規(guī)則,比如絕對不要在線程間傳遞 managed objects等。這并不單是說你絕不應該在另一個線程中去更改某個其他線程的 managed object ,甚至是讀取其中的屬性都是不能做的。要想傳遞這樣的對象,正確做法是通過傳遞它的 object ID ,然后從其他對應線程所綁定的 context 中去獲取這個對象。

其實只要你遵循那些規(guī)則,并使用這篇文章里所描述的方法的話,處理 Core Data 的并行編程還是比較容易的。

Xcode 所提供的 Core Data 標準模版中,所設立的是運行在主線程中的一個存儲調(diào)度 (persistent store coordinator)和一個托管對象上下文 (managed object context) 的方式。在很多情況下,這種模式可以運行良好。創(chuàng)建新的對象和修改已存在的對象開銷都非常小,也都能在主線程中沒有困難地完成。然后,如果你想要做大量的處理,那么把它放到一個后臺上下文來做會比較好。一個典型的應用場景是將大量數(shù)據(jù)導入到 Core Data 中。

我們的方式非常簡單,并且可以被很好地描述:

  1. 我們?yōu)閷牍ぷ鲉为殑?chuàng)建一個操作
  2. 我們創(chuàng)建一個 managed object context ,它和主 managed object context 使用同樣的 persistent store coordinator
  3. 一旦導入 context 保存了,我們就通知 主 managed object context 并且合并這些改變

示例app中,我們要導入一大組柏林的交通數(shù)據(jù)。在導入的過程中,我們展示一個進度條,如果耗時太長,我們希望可以取消當前的導入操作。同時,我們顯示一個隨著數(shù)據(jù)加入可以自動更新的 table view 來展示目前可用的數(shù)據(jù)。示例用到的數(shù)據(jù)是采用的 Creative Commons license 公開的,你可以在此下載它們。這些數(shù)據(jù)遵守一個叫做 General Transit Feed 格式的交通數(shù)據(jù)公開標準。

我們創(chuàng)建一個 NSOperation 的子類,將其叫做 ImportOperation,我們通過重寫 main 方法,用來處理所有的導入工作。這里我們使用 NSPrivateQueueConcurrencyType 來創(chuàng)建一個獨立并擁有自己的私有 dispatch queue 的 managed object context,這個 context 需要管理自己的隊列。在隊列中的所有操作必須使用 performBlock 或者 performBlockAndWait 來進行觸發(fā)。這點對于保證這些操作能在正確的線程上執(zhí)行是相當重要的。

NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[self.context performBlockAndWait:^
{
    [self import];
}];

在這里我們重用了已經(jīng)存在的 persistent store coordinator 。一般來說,初始化 managed object contexts 要么使用 NSPrivateQueueConcurrencyType,要么使用 NSMainQueueConcurrencyType。第三種并發(fā)類型 NSConfinementConcurrencyType 是為老舊代碼準備的,我們不建議再使用它了。

在導入前,我們枚舉文件中的各行,并對可以解析的每一行創(chuàng)建 managed object :

[lines enumerateObjectsUsingBlock:
  ^(NSString* line, NSUInteger idx, BOOL* shouldStop)
  {
      NSArray* components = [line csvComponents];
      if(components.count < 5) {
          NSLog(@"couldn't parse: %@", components);
          return;
      }
      [Stop importCSVComponents:components intoContext:context];
  }];

在 view controller 中通過以下代碼來開始操作:

ImportOperation* operation = [[ImportOperation alloc]
     initWithStore:self.store fileName:fileName];
[self.operationQueue addOperation:operation];

至此為止,后臺導入部分已經(jīng)完成。接下來,我們要加入取消功能,這其實非常簡單,只需要枚舉的 block 中加一個判斷就行了:

if(self.isCancelled) {
    *shouldStop = YES;
    return;
}

最后為了支持進度條,我們在 operation 中創(chuàng)建一個叫做 progressCallback 的屬性。需要注意的是,更新進度條必須在主線程中完成,否則會導致 UIKit 崩潰。

operation.progressCallback = ^(float progress)
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^
    {
        self.progressIndicator.progress = progress;
    }];
};

我們在枚舉中來調(diào)用這個進度條更新的 block 的操作:

self.progressCallback(idx / (float) count);

然而,如果你執(zhí)行示例代碼的話,你會發(fā)現(xiàn)它運行逐漸變得很慢,取消操作也有遲滯。這是因為主操作隊列中塞滿了要更新進度條的 block 操作。一個簡單的解決方法是降低更新的頻度,比如只在每導入一百行時更新一次:

NSInteger progressGranularity = 100;

if (idx % progressGranularity == 0) {
    self.progressCallback(idx / (float) count);
}

更新 Main Context

在 app 中的 table view 是由一個在主線程上獲取了結果的 controller 所驅(qū)動的。在導入數(shù)據(jù)的過程中和導入數(shù)據(jù)完成后,我們要在 table view 中展示我們的結果。

在讓一切運轉起來之前之前,還有一件事情要做?,F(xiàn)在在后臺 context 中導入的數(shù)據(jù)還不能傳送到主 context 中,除非我們顯式地讓它這么去做。我們在 Store 類的設置 Core Data stack 的 init 方法中加入下面的代碼:

[[NSNotificationCenter defaultCenter]
    addObserverForName:NSManagedObjectContextDidSaveNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification* note)
{
    NSManagedObjectContext *moc = self.mainManagedObjectContext;
    if (note.object != moc)
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }];
}];

如果 block 在主隊列中被作為參數(shù)傳遞的話,那么這個 block 也會在主隊列中被執(zhí)行。如果現(xiàn)在你運行程序的話,你會注意到 table view 會在完成導入數(shù)據(jù)后刷新數(shù)據(jù),但是這個行為會阻塞用戶大概幾秒鐘。

要修正這個問題,我們需要做一些無論如何都應該做的事情:批量保存。在導入較大的數(shù)據(jù)時,我們需要定期保存,逐漸導入,否則內(nèi)存很可能就會被耗光,性能一般也會更壞。而且,定期保存也可以分散主線程在更新 table view 時的工作壓力。

合理的保存的次數(shù)可以通過試錯得到。保存太頻繁的話,可能會在 I/O 操作上花太多時間;保存次數(shù)太少的話,應用會變得無響應。在經(jīng)過一些嘗試后,我們設定每 250 次導入就保存一次。改進后,導入過程變得很平滑,它可以適時更新 table view,也沒有阻塞主 context 太久。

其他考慮

在導入操作時,我們將整個文件都讀入到一個字符串中,然后將其分割成行。這種處理方式對于相對小的文件來說沒有問題,但是對于大文件,最好采用惰性讀取 (lazily read) 的方式逐行讀入。本文最后的示例將使用輸入流的方式來實現(xiàn)這個特性,在 StackOverflow 上 Dave DeLong 也提供了一段非常好的示例代碼來說明這個問題。

在 app 第一次運行時,除開將大量數(shù)據(jù)導入 Core Data 這一選擇以外,你也可以在你的 app bundle 中直接放一個 sqlite 文件,或者從一個可以動態(tài)生成數(shù)據(jù)的服務器下載。如果使用這些方式的話,可以節(jié)省不少在設備上的處理時間。

最后,最近對于 child contexts 有很多爭議。我們的建議是不要在后臺操作中使用它。如果你以主 context 的 child 的方式創(chuàng)建了一個后臺 context 的話,保存這個后臺 context 將阻塞主線程。而要是將主 context 作為后臺 context 的 child 的話,實際上和與創(chuàng)建兩個傳統(tǒng)的獨立 contexts 來說是沒有區(qū)別的。因為你仍然需要手動將后臺的改變合并回主 context 中去。

設置一個 persistent store coordinator 和兩個獨立的 contexts 被證明了是在后臺處理 Core Data 的好方法。除非你有足夠好的理由,否則在處理時你應該堅持使用這種方式。

擴展閱讀:

后臺 UI 代碼

首先要強調(diào):UIKit 只能在主線程上運行。而那部分不與 UIKit 直接相關,卻會消耗大量時間的 UI 代碼可以被移動到后臺去處理,以避免其將主線程阻塞太久。但是在你將你的 UI 代碼移到后臺隊列之前,你應該好好地測量哪一部分才是你代碼中的瓶頸。這非常重要,否則你所做的優(yōu)化根本是南轅北轍。

如果你找到了你能夠隔離出的昂貴操作的話,可以將其放到操作隊列中去:

__weak id weakSelf = self;
[self.operationQueue addOperationWithBlock:^{
    NSNumber* result = findLargestMersennePrime();
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        MyClass* strongSelf = weakSelf;
        strongSelf.textLabel.text = [result stringValue];
    }];
}];

如你所見,這些代碼其實一點也不直接明了。我們首先聲明了一個 weak 引用來參照 self,否則會形成循環(huán)引用( block 持有了 self,私有的 operationQueue retain 了 block,而 self 又 retain 了 operationQueue )。為了避免在運行 block 時訪問到已被釋放的對象,在 block 中我們又需要將其轉回 strong 引用。

編者注 這在 ARC 和 block 主導的編程范式中是解決 retain cycle 的一種常見也是最標準的方法。

后臺繪制

如果你確定 drawRect: 是你的應用的性能瓶頸,那么你可以將這些繪制代碼放到后臺去做。但是在你這樣做之前,檢查下看看是不是有其他方法來解決,比如、考慮使用 core animation layers 或者預先渲染圖片而不去做 Core Graphics 繪制??梢钥纯?Florian 對在真機上圖像性能測量的帖子,或者可以看看來自 UIKit 工程師 Andy Matuschak 對個各種方式的權衡的評論。

如果你確實認為在后臺執(zhí)行繪制代碼會是你的最好選擇時再這么做。其實解決起來也很簡單,把 drawRect: 中的代碼放到一個后臺操作中去做就可以了。然后將原本打算繪制的視圖用一個 image view 來替換,等到操作執(zhí)行完后再去更新。在繪制的方法中,使用 UIGraphicsBeginImageContextWithOptions 來取代 UIGraphicsGetCurrentContext

UIGraphicsBeginImageContextWithOptions(size, NO, 0);
// drawing code here
UIImage *i = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return i;

通過在第三個參數(shù)中傳入 0 ,設備的主屏幕的 scale 將被自動傳入,這將使圖片在普通設備和 retina 屏幕上都有良好的表現(xiàn)。

如果你在 table view 或者是 collection view 的 cell 上做了自定義繪制的話,最好將它們放入 operation 的子類中去。你可以將它們添加到后臺操作隊列,也可以在用戶將 cell 滾動出邊界時的 didEndDisplayingCell 委托方法中進行取消。這些技巧都在 2012 年的WWDC Session 211 -- Building Concurrent User Interfaces on iOS中有詳細闡述。

除了在后臺自己調(diào)度繪制代碼,以也可以試試看使用 CALayerdrawsAsynchronously 屬性。然而你需要精心衡量這樣做的效果,因為有時候它能使繪制加速,有時候卻適得其反。

異步網(wǎng)絡請求處理

你的所有網(wǎng)絡請求都應該采取異步的方式完成。

然而,在 GCD 下,有時候你可能會看到這樣的代碼

// 警告:不要使用這些代碼。
dispatch_async(backgroundQueue, ^{
   NSData* contents = [NSData dataWithContentsOfURL:url]
   dispatch_async(dispatch_get_main_queue(), ^{
      // 處理取到的日期
   });
});

乍看起來沒什么問題,但是這段代碼卻有致命缺陷。你沒有辦法去取消這個同步的網(wǎng)絡請求。它將阻塞住線程直到它完成。如果請求一直沒結果,那就只能干等到超時(比如 dataWithContentsOfURL: 的超時時間是 30 秒)。

如果隊列是串行執(zhí)行的話,它將一直被阻塞住。假如隊列是并行執(zhí)行的話,GCD 需要重開一個線程來補湊你阻塞住的線程。兩種結果都不太妙,所以最好還是不要阻塞線程。

要解決上面的困境,我們可以使用 NSURLConnection 的異步方法,并且把所有操作轉化為 operation 來執(zhí)行。通過這種方法,我們可以從操作隊列的強大功能和便利中獲益良多:我們能輕易地控制并發(fā)操作的數(shù)量,添加依賴,以及取消操作。

然而,在這里還有一些事情值得注意: NSURLConnection 是通過 run loop 來發(fā)送事件的。因為時間發(fā)送不會花多少時間,因此最簡單的是就只使用 main run loop 來做這個。然后,我們就可以用后臺線程來處理輸入的數(shù)據(jù)了。

另一種可能的方式是使用像 AFNetworking 這樣的框架:建立一個獨立的線程,為建立的線程設置自己的 run loop,然后在其中調(diào)度 URL 連接。但是并不推薦你自己去實現(xiàn)這些事情。

要處理URL 連接,我們重寫自定義的 operation 子類中的 start 方法:

- (void)start
{
    NSURLRequest* request = [NSURLRequest requestWithURL:self.url];
    self.isExecuting = YES;
    self.isFinished = NO;
    [[NSOperationQueue mainQueue] addOperationWithBlock:^
    {
        self.connection = [NSURLConnectionconnectionWithRequest:request
                                                       delegate:self];
    }];
}

由于重寫的是 start 方法,所以我們需要自己要管理操作的 isExecutingisFinished 狀態(tài)。要取消一個操作,我們需要取消 connection ,并且設定合適的標記,這樣操作隊列才知道操作已經(jīng)完成。

- (void)cancel
{
    [super cancel];
    [self.connection cancel];
    self.isFinished = YES;
    self.isExecuting = NO;
}

當連接完成加載后,它向代理發(fā)送回調(diào):

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    self.data = self.buffer;
    self.buffer = nil;
    self.isExecuting = NO;
    self.isFinished = YES;
}

就這么多了。完整的代碼可以參見GitHub上的示例工程

總結來說,我們建議要么你花時間來把事情做對做好,要么就直接使用像 AFNetworking 這樣的框架。其實 AFNetworking 還提供了不少好用的小工具,比如有個 UIImageView 的 category,來負責異步地從一個 URL 加載圖片。在你的 table view 里使用的話,還能自動幫你處理取消加載操作,非常方便。

擴展閱讀:

進階:后臺文件 I/O

在之前我們的后臺 Core Data 示例中,我們將一整個文件加載到了內(nèi)存中。這種方式對于較小的文件沒有問題,但是受限于 iOS 設備的內(nèi)存容量,對于大文件來說的話就不那么友好了。要解決這個問題,我們將構建一個類,它負責一行一行讀取文件而不是一次將整個文件讀入內(nèi)存,另外要在后臺隊列處理文件,以保持應用相應用戶的操作。

為了達到這個目的,我們使用能讓我們異步處理文件的 NSInputStream 。根據(jù)官方文檔的描述:

如果你總是需要從頭到尾來讀/寫文件的話,streams 提供了一個簡單的接口來異步完成這個操作

不管你是否使用 streams,大體上逐行讀取一個文件的模式是這樣的:

  1. 建立一個中間緩沖層以提供,當沒有找到換行符號的時候可以向其中添加數(shù)據(jù)
  2. 從 stream 中讀取一塊數(shù)據(jù)
  3. 對于這塊數(shù)據(jù)中發(fā)現(xiàn)的每一個換行符,取中間緩沖層,向其中添加數(shù)據(jù),直到(并包括)這個換行符,并將其輸出
  4. 將剩余的字節(jié)添加到中間緩沖層去
  5. 回到 2,直到 stream 關閉

為了將其運用到實踐中,我們又建立了一個示例應用,里面有一個 Reader 類完成了這件事情,它的接口十分簡單

@interface Reader : NSObject
- (void)enumerateLines:(void (^)(NSString*))block
            completion:(void (^)())completion;
- (id)initWithFileAtPath:(NSString*)path;
@end

注意,這個類不是 NSOperation 的子類。與 URL connections 類似,輸入的 streams 通過 run loop 來傳遞它的事件。這里,我們?nèi)匀徊捎?main run loop 來分發(fā)事件,然后將數(shù)據(jù)處理過程派發(fā)至后臺操作線程里去處理。

- (void)enumerateLines:(void (^)(NSString*))block
            completion:(void (^)())completion
{
    if (self.queue == nil) {
        self.queue = [[NSOperationQueue alloc] init];
        self.queue.maxConcurrentOperationCount = 1;
    }
    self.callback = block;
    self.completion = completion;
    self.inputStream = [NSInputStream inputStreamWithURL:self.fileURL];
    self.inputStream.delegate = self;
    [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                forMode:NSDefaultRunLoopMode];
    [self.inputStream open];
}

現(xiàn)在,input stream 將(在主線程)向我們發(fā)送代理消息,然后我們可以在操作隊列中加入一個 block 操作來執(zhí)行處理了:

- (void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode) {
        ...
        case NSStreamEventHasBytesAvailable: {
            NSMutableData *buffer = [NSMutableData dataWithLength:4 * 1024];
            NSUInteger length = [self.inputStream read:[buffer mutableBytes]
                                             maxLength:[buffer length]];
            if (0 < length) {
                [buffer setLength:length];
                __weak id weakSelf = self;
                [self.queue addOperationWithBlock:^{
                    [weakSelf processDataChunk:buffer];
                }];
            }
            break;
        }
        ...
    }
}

處理數(shù)據(jù)塊的過程是先查看當前已緩沖的數(shù)據(jù),并將新加入的數(shù)據(jù)附加上去。接下來它將按照換行符分解成小的部分,并處理每一行。

數(shù)據(jù)處理過程中會不斷的從buffer中獲取已讀入的數(shù)據(jù)。然后把這些新讀入的數(shù)據(jù)按行分開并存儲。剩余的數(shù)據(jù)被再次存儲到緩沖區(qū)中:

- (void)processDataChunk:(NSMutableData *)buffer;
{
    if (self.remainder != nil) {
        [self.remainder appendData:buffer];
    } else {
        self.remainder = buffer;
    }
    [self.remainder obj_enumerateComponentsSeparatedBy:self.delimiter
                                            usingBlock:^(NSData* component, BOOL last) {
        if (!last) {
            [self emitLineWithData:component];
        } else if (0 < [component length]) {
            self.remainder = [component mutableCopy];
        } else {
            self.remainder = nil;
        }
    }];
}

現(xiàn)在你運行示例應用的話,會發(fā)現(xiàn)它在響應事件時非常迅速,內(nèi)存的開銷也保持很低(在我們測試時,不論讀入的文件有多大,堆所占用的內(nèi)存量始終低于 800KB)。絕大部分時候,使用逐塊讀入的方式來處理大文件,是非常有用的技術。

延伸閱讀:

總結

通過我們所列舉的幾個示例,我們展示了如何異步地在后臺執(zhí)行一些常見任務。在所有的解決方案中,我們盡力保持了代碼的簡單,這是因為在并發(fā)編程中,稍不留神就會捅出簍子來。

很多時候為了避免麻煩,你可能更愿意在主線程中完成你的工作,在你能這么做事,這確實讓你的工作輕松不少,但是當你發(fā)現(xiàn)性能瓶頸時,你可以嘗試盡可能用最簡單的策略將那些繁重任務放到后臺去做。

我們在上面例子中所展示的方法對于其他任務來說也是安全的選擇。在主隊列中接收事件或者數(shù)據(jù),然后用后臺操作隊列來執(zhí)行實際操作,然后回到主隊列去傳遞結果,遵循這樣的原則來編寫盡量簡單的并行代碼,將是保證高效正確的不二法則。