本文由CocoaChina譯者ALEX吳浩文翻譯
作者:HOSSAM GHAREEB
原文:iOS Concurrency: Getting Started with NSOperation and Dispatch Queues
在iOS開發中,並行一直被認為是項目裡的怪物。它被認為是一個危險的區域,許多開發者盡力去避免的區域。有謠傳說多線程代碼應盡可能的避免。我同意並行是危險的,不過那只是因為你沒有很好地理解。只是因為未知才變得危險。想想人們在生活中危險的行為活動,有很多吧?但是一旦掌握了,就變得簡單了。並行是雙刃劍,你應該學習並且掌握如何使用它。它幫助你編寫高效、執行快速和響應靈敏的應用,但與此同時,濫用它會無情地毀了你的應用。因此在開始編寫任何並行代碼之前,先想想你為什麼需要並行以及你需要使用哪個API來解決這個問題?在iOS中我們可以使用不同的API。本教程我們將討論兩個最常用的API——NSOperation和調度隊列。
我們為什麼需要並行?
我知道你是一個優秀的有經驗的iOS開發者。然而,無論構建什麼樣的應用程序,你需要知道並行能使你的應用響應更靈敏、運行更快速。在這裡我總結了幾點學習或使用並行的優點:
利用iOS設備的硬件:現在所有的iOS設備都有一個多核的處理器,它使開發人員可以並行執行多個任務。你應該利用這個特性,獲得硬件帶來的好處。
更好的用戶體驗:你大概已經寫過代碼來調用web服務,處理一些IO,或執行任何繁重的任務。你知道,在UI線程執行這些操作將卡住應用,使其未響應。一旦用戶面臨這種情況,第一步,他/她將殺死/關閉應用,沒有任何其它想法。有了並行,所有這些任務可以在後台完成,不會阻塞主線程,不會打擾用戶。他們仍可以點擊按鈕、滾動和浏覽你的應用,而在後台處理繁重的任務。
NSOperation和調度隊列之類的API使得並行容易使用:創建和管理線程並不是容易的任務。這就是為什麼大部分開發者聽到並行和多線程的代碼會感到害怕的原因。在iOS中,我們有很棒的易用的並行API,它將使你的編程變得更容易。你不需要關心創建線程或管理任何底層的東西。API將為你完成所有這些任務。這些API的另一個重要優勢是,它可以幫助你輕松實現同步來避免競態條件。競態條件發生在多個線程試圖訪問共享資源時,這會導致意想不到的結果。通過使用同步,能夠保護資源免受線程間共享的影響。
關於並行你需要了解什麼?
在本教程中,我們將解釋所有關於並行你需要的了解的,並減輕你所有關於並行的恐懼。首先我們推薦看看block(在Swift中是閉包),因為大量並行API使用它。接著我們將談談調度隊列和NSOperationQueues。我們將帶你了解一下每個並行的概念,它們的不同點,以及如何實現它們。
第1部分:GCD(偉大的中樞調度)
GCD是最常用的管理並行代碼和執行異步操作的Unix系統層的API。GCD構造和管理隊列中的任務。首先,讓我們看看隊列是什麼。
隊列是什麼?
隊列是按先進先出(FIFO)管理對象的數據結構。隊列類似電影院的售票窗口,票的銷售是誰先到誰先服務。在等待線前面的人先去買他們的門票,在其余的後抵達的人之前。隊列在計算機科學中是相似的,因為第一個添加到隊列的對象也是第一個從隊列中刪除的對象。
調度隊列
調度隊列是一種簡單的異步和同步任務的方法。它們是隊列,任務以block的形式被你的應用提交到其中。有兩種調度隊列:(1)串行隊列(2)並行隊列。在談不同點之前,你需要知道任務分配給這兩個隊列都是在單獨的線程執行的,而不是在創建任務的線程上。換句話說,你創建block再提交到主線程的調度隊列。但所有這些任務(block)將運行在單獨的線程,而不是主線程。
串行隊列
當你選擇創建一個串行隊列,隊列一次只能執行一個任務。同一串行隊列的所有任務將相互尊重並連續執行。然而,它們不關心任務是不是在單獨的線程,這意味著你仍然可以通過使用多個串行隊列來並行地執行任務。例如,你可以創建兩個串行隊列,每個隊列一次只執行一個任務,不過多達兩個任務仍可並行執行。
串行隊列用於管理共享資源是極好的。它保證了序列化訪問共享資源,防止了競態條件。想象一個售票亭,有一群人想買電影票,這裡的展台的工作人員就是一個共享資源。如果員工需要為這些人在同一時間服務,這將是混亂的。為了處理這種情況,人們需要排隊(串行隊列),這樣員工可以一次服務一個顧客。
重申,這並不意味著電影院一次只能處理一個客戶。如果多設置兩個展位,它可以同時處理三個客戶。這就是為什麼說你仍然可以通過使用幾個串行隊列來並行執行多個任務。
使用串行隊列的優點是:
1.保證序列化訪問共享資源,避免競態條件。
2.任務的執行順序是可預測的。當你提交任務到一個串行調度隊列,它們將按插入的順序執行。
3.你可以創建任意數量的串行隊列。
並行隊列
顧名思義,並行隊列可以並行執行多個任務。任務(block)按添加到隊列的順序開始,但它們的執行會同時發生,它們不會相互等待。並行隊列保證任務開始的順序,但你不知道執行的順序、執行時間或某個時間點的正在執行的任務數。
例如,你提交三個任務(任務#1,#2和#3)到並行隊列。任務將並行執行並且按添加到隊列的順序開始。然而,執行時間和完成時間各不相同。即使任務#2和#3的開始需要一些時間,在任務#1之前它們都可以完成。任務的執行是由系統決定的。
使用隊列
既然我們已經解釋了串行和並行隊列,是時候看看我們可以如何使用它們。默認情況下,系統為每個應用提供了一個串行隊列和四個並行隊列。主調度隊列是全局可用的串行隊列,它在應用的主線程執行任務。它是用來更新應用的UI和執行所有與UI視圖更新有關的任務。同時只有一個任務執行,這就是為什麼當你在主隊列執行一個繁重的任務UI會被卡住。
除了主隊列,系統提供4個並行隊列。我們稱之為全局調度隊列。這些隊列對於應用是全局的,區別只在於它們的優先級。使用一個全局並行隊列,你必須得到隊列的引用,使用函數dispatch_get_global_queue,它的第一個參數是:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
這些隊列類型代表了執行的優先級。HIGH的隊列擁有最高的優先級,BACKGROUND擁有最低的優先級。所以你可以根據任務的優先級決定你使用的隊列。也請注意,這些隊列也被蘋果API使用,所以你的任務並不是這些隊列裡唯一的任務。
最後,你可以創建任意數量的串行或並行隊列。關於並行隊列,我強烈推薦使用的那四個全局隊列,不過你也可以創建自己的。
GCD備忘表
現在,你應該對調度隊列有了一個基本的了解。我要給你一個簡單備忘表,供你參考。表非常簡單,包含了關於GCD你需要知道的所有信息。
很酷,對吧?現在讓我們做一個簡單的演示,來看看如何使用調度隊列。我將向你展示如何利用調度隊列來優化應用性能,使其響應更靈敏。
演示項目
我們的開始項目非常簡單,我們顯示四個image view,每一個從遠程站點請求一個特定的形象。圖像請求在主線程中完成,來向你們展示這將如何影響界面的響應,我在圖片下面添加了一個簡單的滑塊。現在下載並運行開始項目。單擊Start按鈕啟動圖像下載並在圖像下載時拖動滑動條。你會發現你一點也拖不動。
一旦你點擊Start按鈕,圖像在主線程開始下載。顯然,這種方法非常糟糕,它使UI沒有響應。遺憾的是直到今天仍有一些應用在主線程執行繁重的加載任務。現在我們要修復它,使用調度隊列。
首先,我們將實現並行隊列的解決方案,然後再用串行隊列。
使用並行調度隊列
現在回到Xcode項目裡的ViewController.swift。如果你分析了代碼,你應該看到didClickOnStart動作方法。該方法處理圖像下載。我們現在是這樣執行任務的:
@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 }
每個下載器被認為是一個任務,現在所有的任務都在主隊列執行。現在讓我們得到一個Default優先級的全局並行隊列的引用。
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,得到一個默認並行隊列的引用,然後在block裡我們提交一個任務來下載第一個圖像。一旦圖像下載完成,我們提交另一個任務到主隊列來用下載的圖像更新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()方法。這次我們將使用一個串行隊列來下載圖片。在使用串行隊列時,你需要密切關注你正在引用的串行隊列是哪一個。每個應用都有一個默認的串行隊列,這實際上是用於UI的主隊列。所以記住當使用串行隊列時,你必須創建一個新隊列,否則會在應用試圖執行更新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 }) } }
正如我們看到的,與並行隊列唯一不同的地方是串行隊列的創建。當構建並再次運行應用時,你將再次看到圖片在後台下載,所以你可以繼續與用戶界面交互。
但你會注意到兩件事:
1.和並行隊列相比,下載圖片需要的時間有點長。這是因為我們同時只加載一個圖像。每個任務等待前面的任務完成才會被執行。
2.圖像按image1,image2,image3,image4的順序加載。因為隊列是一個串行隊列,它一次執行一個任務。
第2部分:操作隊列
GCD是一個底層的C的API,它使開發人員能夠並行地執行任務。操作隊列,另一方面,是高度抽象的隊列模型,是建立在GCD之上的。這意味著你可以並行執行任務就像GCD一樣,但以面向對象的方式。簡而言之,隊列操作讓編程更加簡單。
不同於GCD,它們不按先進先出的順序。下面是操作隊列和調度隊列的不同點:
1.不遵循先進先出:在操作隊列中,你可以設置一個操作的執行優先級,你可以添加操作之間的依賴關系,這意味著你可以定義一些操作完成後才會執行其他操作。這就是為什麼它們不遵循先進先出。
2.默認情況下,它們同時操作:然而你不能把它的類型改變成串行隊列。通過使用操作之間的依賴關系,在操作隊列還存在一個工作區來依次執行任務。
3.操作隊列是類NSOperationQueue的實例,其任務封裝在NSOperation的實例裡。
NSOperation
任務以NSOperation實例的形式提交到操作隊列。我們在GCD討論了任務是以block提交。同樣這裡也可以這樣做但應捆綁在NSOperation實例裡。你可以簡單地認為NSOperation是單個的工作單元。
NSOperation是一個抽象類,它不能直接使用,所以你必須使用NSOperation子類。在iOS SDK裡,我們提供兩個NSOperation的具體子類。這些類可以直接使用,但你也可以繼承NSOperation來創建自己的類來執行操作。我們可以直接使用的兩個類:
1.NSBlockOperation——使用這個類來用一個或多個block初始化操作。操作本身可以包含多個塊。當所有block被執行操作將被視為完成。
2.NSInvocationOperation——使用這個類來初始化一個操作,它包括指定對象的調用selector。
所以NSOperation的優勢是什麼?
1.首先,它們通過NSOperation類裡的方法addDependency(op:NSOperation)支持依賴。當你需要開始一個依賴於其它操作執行的操作,你會需要NSOperation。
2.其次,你可以通過下面這些值設置屬性queuePriority來改變執行優先級:
public enum NSOperationQueuePriority : Int { case VeryLow case Low case Normal case High case VeryHigh }
優先級高的操作將先被執行。
3.對於任何給定的隊列,你可以取消一個特定的或所有的操作。操作可以在被添加到隊列後被取消。取消是通過調用NSOperation類裡的方法cancel()。當你取消任何操作的時候,我們有三個場景,其中一個會發生:
你的操作已經完成。在這種情況下,取消方法沒有效果。
你的操作已經被執行。在這種情況下,系統不會強制操作代碼停止,而是屬性cancelled被置為true。
你的操作仍在隊列中等待。在這種情況下,你的操作將不會被執行。
4.NSOperation有3個有用的布爾屬性,finished、 cancelled和ready。一旦操作執行完成,finisher將被置為true。一旦操作被取消,cancelled將被置為true。一旦准備即將被執行,ready將被置為true。
5.任何NSOperation有一個選項來設置回調,一旦任務完成將會被調用。在NSOperation裡,一旦屬性finished被置為true,這個block將被調用。
現在讓我們重寫演示項目,但這一次我們將使用NSOperationQueues。首先在ViewController類裡聲明變量:
var queue = NSOperationQueue()
接下來,用下面的代碼替換didClickOnStart方法,看看我們在NSOperationQueue裡如何執行操作:
@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用給定的block(或Swift中的閉包)來創建一個新的操作。這很簡單,不是嗎?要在主隊列執行一項任務,不用調用使用GCD時用的dispatch_async(),我們可以從NSOperationQueue(NSOperationQueue.mainQueue())提交你想要執行的操作到主隊列。
你可以運行這個應用來做一個快速測試。如果輸入的代碼是正確的,應用應該能夠在後台下載圖片並且不會阻塞用戶界面。
在前面的示例中,我們使用的方法addOperationWithBlock在隊列中添加操作。讓我們看看如何使用NSBlockOperation做相同的事,而且與此同時,它為我們提供更多功能和選項,如設置回調。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) }
對於每個操作,我們創建一個新的NSBlockOperation的實例來將任務封裝到一個block。通過使用NSBlockOperation,你得以設置回調。現在當操作完成,回調將被調用。為簡單起見,我們只輸出一個簡單消息來表示操作完成。如果你運行這個演示,你將在控制台看到這樣的東西:
Operation 1 completed Operation 3 completed Operation 2 completed Operation 4 completed
取消操作
如前所述,NSBlockOperation允許你管理操作。現在讓我們看看如何取消操作。要做到這一點,首先添加一個按鈕到導航欄並把它命名Cancel。為了說明取消操作,我們將添加操作#2和操作#1之間的依賴,以及操作#3和操作#2之間的依賴。這意味著操作#2將在操作#1完成後開始,操作#3將在操作#2完成後開始。操作#4沒有依賴,它將並行工作。為了取消操作,你所有需要做的就是調用NSOperationQueue的cancelAllOperations()。在ViewController類中插入下面的方法:
@IBAction func didClickOnCancel(sender: AnyObject) { self.queue.cancelAllOperations() }
記住你需要把你添加到導航欄的Cancel按鈕關聯到didClickOnCancel方法。為此你可以回到Main.storyboard文件打開Connections Inspector。在那裡你會在Received Actions部分看到取消關聯didSelectCancel()。單擊+拖動從空圓到Cancel工具欄按鈕。然後在didClickOnStart方法創建依賴是這樣的:
operation2.addDependency(operation1) operation3.addDependency(operation2)
接著改變操作#1的回調來輸出取消狀態:
operation1.completionBlock = { print("Operation 1 completed, cancelled:\(operation1.cancelled) ") }
你可以改變操作#2、#3和#4的日志語句,這樣你將更好地理解這個過程。現在讓我們構建並運行。點擊Start按鈕後,按下Cancel按鈕。這將在操作#1完成後取消所有操作。發生了以下的事:
由於操作#1已經執行,取消將什麼也不做。這就是為什麼cancelled值輸出為false,而且應用仍然顯示圖像#1。
如果你點擊Cancel按鈕的速度足夠快,操作#2被取消了。cancelAllOperations()將停止執行,所以圖像#2沒有被下載。
操作#3已經在隊列中,等待操作#2完成。因為它依賴於操作#2的完成然而操作#2被取消,操作#3將不會被執行並立即從隊列中被剔除。
沒有依賴配置到操作#4。它就並發下載圖片#4。
接下來呢?
在本教程中,我帶你了解了iOS並發的概念,以及如何在iOS實現它。我給了你一個很好的並發的介紹,解釋了GCD,展示了如何創建串行和並發隊列。此外,我們還看了NSOperationQueues。你現在應該熟悉GCD和NSOperationQueue之間的區別。
要進一步深入iOS並發,我建議你看看Apple’s Concurrency Guide。
為了參考,你可以在Github的iOS並發庫找到完整的本文提到的源代碼。
請隨意問任何問題。我喜歡閱讀你的評論。
更多譯者翻譯文章,請查看:http://www.cocoachina.com/special/translation/
本文僅用於學習和交流目的,轉載請注明文章譯者、出處和本文鏈接。
感謝博文視點對本期活動的支持