- 原文链接 : iOS Concurrency: Getting Started with NSOperation and Dispatch Queues
- 原文作者 : Ghareeb Hossam
- 译文出自 : APPCODA
- 译者 : kmyhy
並行編程永遠是 iOS 開發中的重要內容。同時也是開發者們必須極力避免的“深水區”。如果你對它沒有一個深刻的理解,那它對於你來說確實很危險。未知的東西總是被認為是危險的。想像一下人們在生活中碰到的各種危險,有多少是真正的危險?一旦人們真正了解了這些危險,這些所謂的危險其實不值一提。並行編程是一柄雙刃劍,你必須學會如何正確地使用和掌握它。它能讓你編寫出高效、快速和響應式的 App,但同時,如果使用不當,它會給你的 App 帶來一場災難。所以,在我們開始編寫任何並行編程代碼之前,首先來思考一下:你為什麼需要並行編程?以及你應該使用哪個 API 來解決問題?在 iOS 中,我們可以使用不同的 API。本教程將介紹其中兩個最常用的 API ── NSOperation 和 Dispatch Queue。
假設你擁有豐富的 iOS 編程經驗。但不管你要創建的是何種類型的 App,你都應該知道並行編程能讓你的 App 跑得更快和更加具有響應式風格。這裡,我來介紹幾個關於學習和使用並行編程的好處:
在本教程中,我們將向你詳細介紹關於並行編程的所有必要知識,同時解除你對它的所有疑慮。首先我們建議你先看一下關於塊(Swift 閉包)的內容,因為塊在並行 API 中使用得非常普遍。然後再來看我們對 dispatch queue 和 NSOperationQueue 的介紹。我們將逐一介紹這兩個 API 的概念,它們的區別,以及如何使用它們。
GCD 是最接近於操作系統 Unix 底層的,最常見的處理並行代碼和執行異步操作的 API。GCD 負責創建和管理任務隊列。首先讓我們來了解什麼是隊列。
隊列是這樣一種數據結構,它以先進先出(FIFO)的方式來管理所存儲的對象。隊列就像是人們在電影院售票窗口進行排隊,票總是先賣給先到的人。位於隊列前面的人要比位於後面的其他人先買到電影票。在計算機學中的隊列與此類似,第一個添加到隊列中的對象,也是第一個從隊列中移除的對象。
Dispatch 隊列 是一種在 App 中執行異步任務和並行任務的方法。它是这样一種隊列,App 將任務以塊(代碼塊)的方式提交給它。有兩種不同的 Dispatch 隊列:(1)串行隊列,(2)並行隊列。在介紹二者的區別前,你需要首先理解一點,分配給這兩種隊列的任務,其實是在另一個單獨的線程中執行的,而不是在創建它們的線程中執行的。也就是說,你在主線程中創建了一個代碼塊并將之提交給 Dispatch 队列,但所有的任務(代碼塊)都不在主線程而是在另外的線程中運行的。
當創建一個串行隊列時,這個隊列一次只能執行一個任務。在同一個串行隊列中的任務會和平共處并以串行的方式執行。但是并沒有規定所有任務都只能在一個串行隊列中執行,你仍然可以通過使用多個串行隊列的方式来並行地執行任務。舉個例子,你可以同时創建兩個串行隊列,雖然每個隊列一次只執行一個任務,但總體效果就是每次有兩個任務在異步地執行。
串行隊列非常利於處理共享資源。它能保證對共享資源的访问是以串行的方式進行的并有效避免了競爭條件。想像一下,只有一個售票窗口,卻有一堆想買電影票的人,那麼坐在售票窗口處的那個售票員就是一個共享資源。如果這個售票員同時為這麼多人提供服務,那麼可以預料現場將是何等的混亂。要解決這個問題,人們需要排隊(串行隊列),那麼售票員一次只需要給一個人賣票就可以了。
但是,并不是說電影院一次只能賣給一張票給顧客。如果增加兩個售票窗口,電影院就可以一次向三個人賣票。這就是我所說的,你仍然可以使用多個串行隊列並行地執行多個任務。
使用串行隊列的好處包括:
顧名思義,並行隊列允許你以並行的方式執行多個任務。這些任務(代碼塊)一開始的順序是它們加入時的順序,但它們執行時都是並行的,它們不需要等待其它任務。並行隊列只能保證任務一開始的順序是加入時的順序,但無法知道它們的執行順序、執行時間或者某一時刻被執行的任務數。
例如,你提交了三個任務(任務 #1,#2,#3)到同一個並行隊列。這些任務會以並行的方式執行,它們啟動的順序就是它們加入隊列的順序。但是它們的執行時間和完成時間是不盡相同的。甚至可能任務 #2、#3 在 #1 之後啟動了,卻在任務 #1 之前完成。任務的執行由系統決定。
我們已經介紹了串行隊列和並行隊列,現在來看看如何使用它們。默認,系統會提供給每個 App 一個串行隊列和四個並行隊列。主 dispatch 隊列是全局的串行隊列,用於執行主線程中的任務。主 Dispatch 隊列通常用於更新 App 的用戶界面,并執行所有與 UIView 的顯示有關的任務。它一次只會執行一個任務,因此當你在主 Dispatch 隊列中運行繁重任務時,UI 會被阻塞。
除了主 Dispatch 隊列,系統還提供了四個並行隊列。我們稱之為全局 Dispatch 隊列。這些隊列在 App 範圍內是全局的,但它們的優先級各不相同。要使用這些隊列,你需要用 dispatch_get_global_queue 函數獲得一個指定類型隊列的引用,這個函數的第一個參數可以採用以下四個值:
這些值分別指定了並行隊列的四個不同的優先級。HIGH 表示優先級最高,BACKGROUND 表示優先級最低。因此你需要根據任務的優先程度來指定要使用的隊列。注意,這些隊列同時也被蘋果的 API 所使用,因此這些隊列中並不僅僅只有你的自己的任務。
最後,你還可以創建任意數量的串行和並行隊列。對於並行隊列,我強烈建議你使用上述四個全局隊列就可以了,當然,你也可以創建另外的隊列。
現在,你已經基本了解了 Dispatch 隊列。為了便於參考,我列出一個簡單的速查表。這個速查表很簡單,但列表中包含了你應該掌握的所有與 GCD 有關的知識點。
不錯吧?現在讓我們創建一個小小的 Demo,以示範 Dispatch 隊列的用法。我將演示如何使用 Dispatch 隊列來優化 App 的性能,并使 App 更加“響應式”。
我們的起始項目非常簡單,只是顯示四個 Image View,每個 Image View 都會從 Web 上請求一張圖片。Web 請求放在了主線程中。為了演示 UI 在響應性能上所受的影響,我在這些 Image View 下面放了一個滑動條。現在下載并運行起始項目。點擊 Start 按鈕,開始下載圖片。在下載過程中,拖動滑動條。 你會發現滑動條根本無法拖動。
當你點擊 Start 按鈕后,圖片開始在主線程中下載。顯然,這種方式很不好并導致了 UI 停止響應。不幸的是,直到目前為止,仍然有一些 App 以這種方式在主線程中進行繁重的加載操作。接下來,我們將以 Dispatch 隊列來解決這個問題。
我們會先用并行隊列的方式然後再用串行隊列的方式解決這個問題。
在 Xcode 中打開 ViewController.swift 文件。如果你看過代碼,你會發現一個叫 didClickOnStart 的 Action 方法。這個方法用於圖片的下載。在這個方法中我們是這樣做的:
@IBAction func didClickOnStart(sender: AnyObject) { let img1 = Downloader.downloadImageWithURL(imageURLs[0]) self.imageView1.image = img1 let img2 = Downloader.downloadImageWithURL(imageURLs[1]) self.imageView2.image = img2 let img3 = Downloader.downloadImageWithURL(imageURLs[2]) self.imageView3.image = img3 let img4 = Downloader.downloadImageWithURL(imageURLs[3]) self.imageView4.image = img4 }
每個 downloader 都是一個下載任務,同時所有任務目前都是在主線程中操作的。現在,我們從全局的並行隊列中獲得一個默認優先級的隊列。
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue) { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) }
首先獲得默認優先級的並行隊列,并將其引用到變量 dispatch_get_global_queue。在這代碼塊中,我們提交了一個任務用於下載第一個圖片。當圖片下載完成,又向主隊列中提交另一個任務用於將下載好的圖片顯示到 Image View 中。也就是說,我們將圖片下載任務放到了後臺線程中,而將 與 UI 有關的任務放到了主隊列中。
將剩下的圖片以同樣方式下載,代碼最終變成這樣:
@IBAction func didClickOnStart(sender: AnyObject) { let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue) { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) } dispatch_async(queue) { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) dispatch_async(dispatch_get_main_queue(), { self.imageView2.image = img2 }) } dispatch_async(queue) { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) dispatch_async(dispatch_get_main_queue(), { self.imageView3.image = img3 }) } dispatch_async(queue) { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) dispatch_async(dispatch_get_main_queue(), { self.imageView4.image = img4 }) } }
我們將四張圖片的下載以並行方式提交到默認隊列中。現在運行程序,它會運行得更快(如果你遇到任何錯誤,檢查你的代碼是否和上述代碼一致)。注意在下載圖片的同時,你還可以流利地拖動滑動條。
我們還可以用串行隊列來解決這個問題。依然是 ViewController.swift 文件中的 didClickOnStart() 方法。這次我們用串行隊列方式來下載圖片。在使用串行隊列的時候,我們需要特別注意你當前所引用的是哪一個串行隊列。每個 App 都會有一個默認的串行隊列,即 UI 更新所使用的主隊列。因此要注意,在使用串行隊列時,我們必須創建一個新的串行隊列,否則我們的任務會在 App 執行更新 UI 的時候執行你的任務。這將導致錯誤和卡頓感出現,導致用戶體驗變差。我們可以用 dispatch_queue_create 函數創建新隊列,并以前面的方式來提交任務。修改后的代碼如下所示:
@IBAction func didClickOnStart(sender: AnyObject) { let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL) dispatch_async(serialQueue) { () -> Void in let img1 = Downloader .downloadImageWithURL(imageURLs[0]) dispatch_async(dispatch_get_main_queue(), { self.imageView1.image = img1 }) } dispatch_async(serialQueue) { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) dispatch_async(dispatch_get_main_queue(), { self.imageView2.image = img2 }) } dispatch_async(serialQueue) { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) dispatch_async(dispatch_get_main_queue(), { self.imageView3.image = img3 }) } dispatch_async(serialQueue) { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) dispatch_async(dispatch_get_main_queue(), { self.imageView4.image = img4 }) } }
如你所見,唯一不同的事情僅僅是把並行隊列換成了串行隊列。當再次運行程序,你會發現圖片在後臺下載,你仍然可以和 UI 進行交互。
但有兩個地方需要注意:
GCD 是一種底層的 C 語言 API,它允許開發者以並行方式執行任務。而 Operation 隊列相對來說是一種更高級和抽象的隊列模型,產生於 GCD 之上。換句話說,你可以像 GCD 一樣執行並行任務,但卻可以使用面向對象的方式。也就是說,Operation 隊列讓只會讓開發者更加輕鬆。
但與 GCD 不同,Operation 隊列不遵從先進先出的原則。這裡列出了二者的不同之處:
準備提交給 Operation 隊列的任務必須以 NSOperation 實例的方式進行封裝。我們介紹過 GCD 的任務是以塊的形式進行提交。類似地,提交給 Operation 隊列的任務必須封裝在 NSOperation 對象中。你可以簡單地將 NSOperation 視作一個單獨的任務單元。
NSOperation 是一個抽象類,它無法直接使用,因此我們必須對它進行子類化。在 iOS SDK 中,提供了兩種 NSOperation 子類實現。這兩個類能夠直接使用,但你仍然可以直接對 NSOperation 進行子類化,創建自己的子類來執行任務。可以直接使用的兩個子類分別是:
但使用 NSOperation 有什麼好處?
public enum NSOperationQueuePriority : Int { case VeryLow case Low case Normal case High case VeryHigh }高優先級的任務將優先執行。
現在,讓我們修改我們的代碼,讓它用 NSOperationQueue 來重新實現。首先在 ViewController 中聲明一個變量:
var queue = NSOperationQueue()
然後,將 didClickOnStart 方法修改為如下代碼,這段代碼中顯示了如何在 NSOperationQueue 中操作 Operation:
@IBAction func didClickOnStart(sender: AnyObject) { queue = NSOperationQueue() queue.addOperationWithBlock { () -> Void in let img1 = Downloader.downloadImageWithURL(imageURLs[0]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView1.image = img1 }) } queue.addOperationWithBlock { () -> Void in let img2 = Downloader.downloadImageWithURL(imageURLs[1]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView2.image = img2 }) } queue.addOperationWithBlock { () -> Void in let img3 = Downloader.downloadImageWithURL(imageURLs[2]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView3.image = img3 }) } queue.addOperationWithBlock { () -> Void in let img4 = Downloader.downloadImageWithURL(imageURLs[3]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView4.image = img4 }) } }
如上述代碼所示,我們使用 addOperationWithBlock 方法以指定的塊(或者叫做 Swift 閉包)來創建一個新的 Operation 實例。很簡單,是吧?要在主隊列中執行任務,我們用 NSOperationQueue 的 mainQueue() 方法代替 GCD 中的 dispatch_async() 方法來獲取主隊列,然後將要在主隊列中執行的操作提交給主隊列。
你可以運行程序來試一試。如果代碼正確, App 將在後臺下載圖片,同時不會阻塞 UI。
在前面的例子中,我們使用 addOperationWithBlock 方法將 Operation 添加進隊列中。接下來我們看看如何使用 NSBlockOperation 來做同樣的事情。同時,我們可以擁有更多的選項和功能,比如為 Operation 設置一個完成塊。將 didClickOnStart 方法修改為:
@IBAction func didClickOnStart(sender: AnyObject) { queue = NSOperationQueue() let operation1 = NSBlockOperation(block: { let img1 = Downloader.downloadImageWithURL(imageURLs[0]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView1.image = img1 }) }) operation1.completionBlock = { print("Operation 1 completed") } queue.addOperation(operation1) let operation2 = NSBlockOperation(block: { let img2 = Downloader.downloadImageWithURL(imageURLs[1]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView2.image = img2 }) }) operation2.completionBlock = { print("Operation 2 completed") } queue.addOperation(operation2) let operation3 = NSBlockOperation(block: { let img3 = Downloader.downloadImageWithURL(imageURLs[2]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView3.image = img3 }) }) operation3.completionBlock = { print("Operation 3 completed") } queue.addOperation(operation3) let operation4 = NSBlockOperation(block: { let img4 = Downloader.downloadImageWithURL(imageURLs[3]) NSOperationQueue.mainQueue().addOperationWithBlock({ self.imageView4.image = img4 }) }) operation4.completionBlock = { print("Operation 4 completed") } queue.addOperation(operation4) }
對於每個 Operation,我們都創建了一個新的 NSBlockOperation 用於將要執行的任務封裝到塊中。通過 NSBlockOperation,我們還可以設置它的完成塊。當 Operation 執行完后,完成塊將被調用。為求簡便,我們僅僅在完成塊中輸出了一個簡單消息表示 Operation 執行完畢。如果你再次運行程序,你將看到控制台中輸出了如下內容:
Operation 1 completed Operation 3 completed Operation 2 completed Operation 4 completed
正如我們前面提到的,NSBlockOperation 允許你管理 Operation。接下來我們看一看如何取消 Operation。首先,在導航條中添加一個取消按鈕,標題為 Cancel。為了演示 Operation 的取消,我們會在 Operation #2 和 Operation #1 之間、Operation #3 和 Operation #2 之間各自添加一個依賴關係。也就是說,Operation #2 會等待 Operation #1 執行完之後才執行,Operation #3 會等待 Operation #2 執行完之後才執行。Operation #4 則沒有依賴,它是正常的異步操作。要取消所有 Operation,你只需調用 NSOperationQueue 的 cancelAllOperations() 方法。在 ViewController 中新增如下方法:
@IBAction func didClickOnCancel(sender: AnyObject) { self.queue.cancelAllOperations() }
當然,你需要在導航欄中新加入的 Cancel 按鈕和 didClickOnCancel 方法之間創建一個連接。這需要我們回到 Main.storyboard 并使用連接面板。在連接面板中,你將看到 Received Actions 一欄下面有一個未連接的 didSelectCancel() 方法。點擊 + 按鈕,從小圓圈拖一條線到 Cancel 按鈕。然後在 didClickOnStart 方法中增加如下語句以創建依賴:
operation2.addDependency(operation1) operation3.addDependency(operation2)
然後修改 Operation #1 的完成塊中的日誌打印語句為:
operation1.completionBlock = { print("Operation 1 completed, cancelled:\(operation1.cancelled) ") }
同樣,修改 Operation #2、#3 和 #4 的日誌語句,以便我們能理解整個流程。現在運行程序。點擊 Start 按鈕后,立即點擊 Cancel 按鈕。這將在 Operation #1 執行完之後取消所有的 Operation。詳細步驟如下:
在本教程中,我完整地介紹了 iOS 異步編程的概念,以及如何在 iOS 中實現異步。我詳細地介紹了什麼是異步,GCD 以及如何創建串行隊列和並行隊列。同時我們也介紹了 NSOperationQueue。你也了解到了 GDC 和 NSOperationQueue 到底有什麼不同。
要進一步了解 iOS 異步編程,我建議你去讀 蘋果的異步編程指南。
作為參考,你還可以從 Github 上下載本教程中的完整源代碼:iOS 異步編程代碼.
對本文有任何疑問請留言。衷心希望你能對本文發表任何評論。
楊宏焱,男,中國大陸籍人士,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年開始學習蘋果 iOS 開發,精通 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作,著有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》(合作翻譯)等。