iOS併發:入門 NSOperation 和 Dispatch Queues

經朋友推薦,遇到這麼一篇介紹併發的好文章,我把它翻譯過來給大家讀一讀。我非專業翻譯人員,水準有限。差錯在所難免,各位讀者如果發現文內有誤,歡迎留言斧正!
R0uter
文章譯自 appcoda iOS Concurrency: Getting Started with NSOperation and Dispatch Queues

在 iOS 開發當中,併發總是會被看成是怪物級別的東西。它是很多開發者盡可能去避免的危險區域。很多謠言都說你應該盡可能地避免在代碼裡寫多執行緒。我覺得,如果你不是十分瞭解併發的話,它的確挺危險的。但這僅僅是因為未知而變得危險罷了。你想想看人的一生要做多少危險的行為或者說行動,很多對吧? 但是一旦他們掌握了併發,併發就變得不再那麼嚇人了。併發是一把你應該學習如何去使用的雙刃劍。它説明你寫出高效、快速和敏捷的應用,但同時,誤用它也會無情地毀掉你的應用。這就是為什麼在寫併發代碼之前,要先想想為什麼你需要使用併發以及到底該用哪個API 來解決你問題? 在 iOS 裡我們有不同的 API 可用。這個教程裡我們將會討論兩個最常用的 API —— NSOperation 以及 Dispatch Queues。

iOS併發特性

iOS併發特性

我們為什麼需要併發?

我知道你是一個有經驗的 iOS 開發者。無論你要創建何種應用,總之,你都會需要了解並發來讓你的應用更加敏捷和快速。這裡我總結了幾點學習或者使用並發的好處:

  • 利用 iOS 設備的硬件: 現在所有的 iOS 設備都有允許開發者同時執行多任務的多核心處理器。你應該利用這個性能並且從硬件中獲益。
  • 更好的用戶體驗: 你可能會寫代碼來調用 web 服務,處理一些 IO,或者執行任何重度的任務。如你所知,在 UI 線程裡做這些任務會卡住應用,導致應用無響應。一點用戶遇到這樣的情況,他的第一反應一定是強行關閉你的應用。使用並發,所有的這些任務都可以在後台完成而不需要掛起主線程或者打擾你的用戶。應用在後台處理重度加載任務的同時,他們仍舊可以點擊按鈕,滾動導航。
  • 像 NSOperation 和 dispatch Queues 這樣的 API 讓並發更易用: 創建和管理線程不是一個簡單的任務。這就是為什麼大部分的開發者害怕遭遇並發和多線程代碼。在 iOS 裡我們有非常簡單易用的 API 來使用並發而不需要那麼痛苦和崩潰。你不需要關心線程的創建或者管理人和低級的任務。API 就會幫你實現同步並且避免競爭問題。競爭問題會在多線程嘗試訪問共享資源時導致的奇怪結果。通過使用同步,你可以保護在線程之間共享的資源。

關於並發你需要了解什麼?

在這個教程裡,我們會向你解釋關於並發你需要了解的一切並且釋放所以你對它的恐懼。首先作為並發 API 裡重度使用的內容,我們推薦去看一眼 blocks (Swift 裡的閉包)。然後我們將會討論 dispatch queues 和 NSOperationQueues 。我們將會帶你了解並發里的每個概念,不同點以及如何去實現它們。

部分 1: GCD (全局中央調度)

GCD 是管理並發代碼和在系統的 UNIX 層級執行異步任務最常用的 API。GCD 提供和管理任務隊列。首先,我們來看看什麼是隊列。

什麼是隊列?

隊列是以先入先出(FIFO)規則管理對象的數據結構。隊列跟電影院買票窗口前排的隊差不多。票以先到先得的規則出售。隊列前邊的人要比隊列後邊的人先買到票。電腦裡的隊列跟這個差不多是因為添加到隊列裡的第一個對像也是第一個從隊列裡移除的對象。

序列

序列

圖片來自: FreeImages.com/Sigurd Decroos

調度隊列

調度隊列1是執行異步任務和在你的應用裡並發的簡單方法。它們是從你應用的 blocks (代碼塊)提交的任務隊列。有兩種調度隊列:(1)串行隊列2,&(2)並行隊列3。在考慮不同點之前,你需要知道分配給這兩個隊列的任務都會分別在在線程裡執行而不是創建它們的那個線程裡。換句話說,你創建一塊代碼然後在主線程把它提交給調度序列。但所有這些任務(代碼塊)將會在不同的線程執行而不是主線程。

譯註:

[1]調度隊列:調度隊列,對於非必要的情況下,我還是盡可能譯為中文以便理解。
[2]串行隊列:串行隊列
[3]並行隊列:並發隊列

串行隊列

當你選擇以串行來創建隊列時,隊列只能一次執行一個任務。所有的在同一串行隊列裡的任務將會互相參照然後串行執行。總之,它們不關心其他隊列裡的任務也就是說你仍舊可以通過使用多個串行隊列並發地執行任務。不如說,你可以創建兩個串行隊列,每個隊列一次只執行一個任務但是兩個隊列仍然是同時執行的。

串行隊列用來管理共享資源是很棒的。它對共享資源的連續訪問提供了保障並且避免競爭問題。想像一下只有一個售票處但有一堆人想要買電影票,這裡這個售票處就是一個共享資源。如果員工不能一次給所有人提供服務這裡將變得一片混亂。要處理這樣的情況,就要要求人們排隊(串行隊列),這樣員工就能一次給一個客戶提供服務了。

再說一次,這不意味著電影院一次只能給一個客戶提供服務。如果它設立的兩個或者更多的售票處,自然就能同時給更多的客戶提供服務。這就是為什麼我對你說仍舊能通過使用多個串行隊列來同時執行多個任務。

使用串行隊列的好處是:

  1. 保證了對共享資源的串行訪問避免了競爭問題;
  2. 任務以可預測的順序執行。當你把任務提交到調度序列,它們將會以插入的順序執行;
  3. 你可以創建任意數量的串行隊列。

並行隊列

顧名思義,並行隊列允許你同時執行多個任務。任務(代碼塊)以它們添加到隊列的允許啟動。但是它們的執行都是同時並發的並且它們不會等待其他任務才啟動。並行隊列保證任務以相同的順序啟動但你不會知道執行的順序,執行時間或者在特定時間多少任務執行了。

比如說,你提交了三個任務(任務#1,#2和#3)到並行隊列。任務是並發執行的並且以它們添加到隊列的順序啟動。總之,執行時間和完成時間是變化的。就算任務#2和任務#3需要一些時間來啟動,它們都可能在任務#1之前完成。這取決於系統來決定任務的執行。

使用隊列

現在我們已經解釋了串行和並行隊列,是時候來看看我們要如何使用它們了。默認來說,系統給每個應用提供了一個串行隊列和四個並發隊列。主調度隊列是全局可用的串行隊列,它在應用的主線程執行任務。它是送來更新應用的 UI 和執行所有與更新 UIView 相關的任務。一次只能執行一個任務,這就是為什麼 UI 會被你在主序列執行的重度任務鎖阻塞。

除了主隊列,系統還提供了四個並發隊列。我們叫它們全局調度隊列。這些隊列是應用全局的並且只有優先級不同而已。要使用全局並行隊列,你需要通過函數 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 的所有信息。

gcd小抄

gcd小抄

屌屌噠,對吧?我們來搞個簡單的 demo 看看如何使用調度隊列。我將會給你展示如何使用調度隊列來優化應用的性能並且讓它更加敏捷。

Demo 項目

我們的新手項目非常簡單,我們現實四個圖片視圖,每個都需要從遠程站點獲取特定的圖片。圖片獲取在主線程完成。為了給你展示這個是如何影響 UI 響應的,我得在圖片下邊添加一個簡單的滑動條。現在下載然後運行這個新手項目。點擊 Start 按鈕來開始下載圖片同時拖動滑動條。你會發現你根本拖動不了。

並發demo

並發demo

一旦你點擊了開始按鈕,圖片就開始從主線程下載。顯然,這導致了非常不好的結果使得 UI 無響應了。不幸的是知道今天還是有不少應用仍然像這樣在主線程加載重度任務。現在我們要來使用調度隊列修復它。

首先我們將會用並行隊列實現解決辦法然後再用串行隊列。

使用並行調度隊列

現在回到 Xcode 項目中的 的ViewController.迅速 文件。如果你自己讀代碼,你就會看到動作方法 didClickOnStart 。這個方法處理了圖片下載。現在我們這樣執行任務:


每一個下載器被看做一個任務,所有的任務現在都是在主隊列執行的。現在我們獲取四個全局並行隊列之一,那個默認優先級的隊列。


我們首先通過 dispatch_get_global_queue 獲取默認並行隊列的引用,然後在代碼塊裡我們提交下載第一個圖像的任務。一旦圖像下載完成,我們提交一個任務到主隊列來用下載的圖像更新圖像視圖。換句話說,我們把圖像下載任務放到了後台線程,但在主隊列執行 UI 相關的更新。

如果你對剩下的圖像也這麼做,你的代碼應該看起來像這樣:

你剛剛提交了四個圖像下載作為並行任務到默認隊列裡。現在編譯並運行應用,它應該會運行的快了一些(如果你編譯錯誤,檢查代碼確保和上邊的一樣)。注意在下載的同時,你應該也可以拖動滑動條了。

使用串行調度隊列

另一種替代的解決辦法是使用串行隊列。現在,回到 的ViewController.迅速 文件裡的那個相同的 didClickOnStart() 方法。這次我們將會使用串行隊列來下載圖片。當使用串行隊列時,你需要高度注意你在引用的是那個串行隊列。每個應用都有一個默認的串行隊列,它實際上是 UI 的主隊列。所以記住當使用串行隊列時,你必須創建新的隊列,否則你將會在應用用來更新 UI 時執行你的任務。這會導致錯誤和延遲破壞用戶體驗。你可以使用 dispatch_queue_create 函數來創建一個新隊列然後和之前我們做的一樣把所以的任務提交上去。在修改之後,代碼看起來像這樣:


如同我們看到的那樣唯一與並行隊列不同的是串行隊列的創建。當你再次編譯並運行應用,你會看到圖像再次在後台下載,這樣你就可以繼續與 UI 交互了。

但你需要注意兩件事:
  1. 與並行隊列的情況相比,這需要一點時間來下載圖像。原因是我們一次只下載一個圖像。每個任務會等待前邊的任務完成才開始執行。
  2. 圖像以 image1, IMAGE2, 圖像3, 和 image4的次序加載。這是因為隊列是串行隊列它每次執行一個任務。

部分 2: 操作隊列

GCD 是一個低級 C API,它使開發者並發地執行任務。操作隊列4,另一方面,是對隊列模式的高級抽象,而且是建立在 GCD 之上的。這意味著你可以像 GCD 那樣並發執行任務,卻是以面向對象的風格。簡單來說,操作隊列讓開發者更爽一些。

不同於 GCD,它們不遵守先入先出原則。這是操作隊列與調度隊列的不同點:

  1. 不遵守 FIFO:在操作隊列裡,你可以給操作設置操作優先級還可以在操作之間添加依賴這意味著你可以定義某些操作只會在另外的操作完成之後才執行。這是為什麼它們不遵循先入先出;
  2. 默認來說,它們是並發執行的:你不能改變它的類型到串行隊列,但你還是有一個變通的方法來在操作隊列裡順序執行操作的,那就是在操作之間使用依賴;
  3. 操作隊列是 NSOperationQueue 類的實例,它的任務封裝在 的NSOperation 類的實例裡。

的NSOperation

提交到操作隊列的任務是以 的NSOperation 實例的形式提交的。我們在 GCD 裡討論過的,任務以代碼塊的形式提交。這裡也是一樣不過應該捆綁到 的NSOperation 實例裡邊。你可以簡單地把 的NSOperation 想像為工作的單元。

的NSOperation 是一個抽像類它不能直接使用,所以你必須使用 的NSOperation 子類。在 iOS SDK 裡,我們有兩個具體的 的NSOperation 子類。這些類可以直接使用,但你同樣可以子類 的NSOperation 然後創建你自己的類來執行任務。我們能直接使用的那兩個類是:

  1. NSBlockOperation – 使用這個類來用一個或者多個代碼塊初始化操作。操作自身能夠包含不只一個代碼塊,當所有代碼塊執行完畢,操作就算是結束;
  2. NSInvocationOperation – 使用這個類來初始化在特定對象裡調用 選擇 的操作

那 NSOperation 的優勢在哪裡?

1)首先,它們支持在 的NSOperation 類里通過方法 addDependency(: 的NSOperation) 來設置依賴。當你需要啟動依賴其他操作的操作時,你可能得使用 的NSOperation

NSOperation-示例

2)其次,你可以通過設置優先級 queuePriority 為以下值來改變執行優先級:

具有高優先級的操作會最先執行。

3)你可以取消特定的操作或者給定隊列裡的所有操作。操作在添加隊列里之後可以取消。取消可以通過調用 的NSOperation 類裡的 取消() 方法完成。當你取消任何操作,以下三者之一會發生:

  • 你的操作已經執行完畢。這樣的話,取消方法也就無效了;
  • 你的操作已經在執行中。這樣的話,系統不會強制你的操作代碼停止,而是設置 取消 屬性為 true
  • 你的操作還在隊列裡等待執行。這樣的話,你的的操作不會再被執行。

4) 的NSOperation 有三個有用的布爾屬性,它們是 finished , 取消 , 和 準備finished 會在操作執行完畢時設置為 true取消 會在操作已被取消時設置為 true準備 會在操作即將被執行時設置為 true

5)任何 的NSOperation 都有一個選項來設置完成代碼塊,一旦任務完成就會被調用。 的NSOperationfinished 屬性一旦設置為 true ,代碼塊就會被調用。

現在讓我們重新我們的項目demo,這次我們將會使用 NSOperationQueues 。首先在 的ViewController 類裡做如下聲明:


接下來,用下面的代碼替換 didClickOnStart 方法,然後看看我們是如何在 NSOperationQueue 裡執行操作的:


如你所見,你使用 addOperationWithBlock 方法來創建一個新操作帶有給定閉包。這很簡單不是嗎?要在主隊列執行任務而不是像我們使用 GCD 時的 dispatch_async() 我們可以在 NSOperationQueue 裡做同樣的事情( NSOperationQueue.mainQueue() )然後提交你想要在主隊列裡執行的操作。

你可以運行這個應用來快速測試一下。如果一切正常,應用應該能夠在後台下載圖像而不會卡住 UI。
在前面的栗子中,我們使用了 addOperationWithBlock 方法來給隊列添加操作。讓我們來看看如何使用 NSBlockOperation 做同樣的事情,但是同時,還能給予我們功能和選項比如設置完成處理器。 didClickOnStart 方法被重寫成這樣:

我們給每個操作創建一個新的 NSBlockOperation 實例來封裝任務到閉包。通過使用 NSBlockOperation ,你可以設置完成處理器。現在當操作結束,完成處理器將會被調用。簡單來講,我們只是記錄了一個簡單的信息來明確操作已經完成。如果你運行這個demo,你將會在終端裡看到如下的輸出:

取消操作

如同上邊提到的, NSBlockOperation 允許你管理操作。現在讓我們來看看如何取消操作。要這麼做,首先添加一個 酒吧 按鍵 項目 到導航欄然後給它命名為 取消 。要演示取消操作,我們將會添加操作#2和操作#1之間的依賴,以及在操作#3和操作#2之間添加另外一個依賴。這意味著操作#3將會在操作#1完成後執行,操作#3將會在操作#2完成之後執行。操作#4沒有依賴它會並發執行。要取消操作你所需要做的就是調用 NSOperationQueuecancelAllOperations() 方法。在 的ViewController 類裡插入如下方法:


你住你需要把你添加的 取消 按鈕關聯到 didClickOnCancel 方法。你可以通過返回到 主要.故事板 文件打開連接管理器。在那裡你會在Received Actions裡看到未連接的 didSelectCancel() 。點擊 + 從空心圓拖動到 取消 按鈕上。然後在 didClickOnStart 方法裡創建依賴如下:


接下來改變操作#1的完成閉包來記錄 取消 狀態:


你可能需要為操作#2,#3和#4改變log文字,這樣你會對過程有一個更好的認識。現在我們來編譯和運行。在你點擊了 開始 按鈕後,點擊 取消 按鈕。這會取消操作#1完成後的所有操作。這裡是要發生的事情:

    • 對操作#1來說已經執行了,取消也無濟於事。這就是為什麼 取消 值被記錄為 ,所以應用仍然顯示了圖像#1;
    • 如果你點 取消 按鈕足夠快,操作#2會被取消。 cancelAllOperations() 的調用會停止它的執行,所以圖像#2沒有下載;
    • 操作#3已經在隊列裡了,等待操作#2完成。它依賴操作#2的完成然而#3被取消了,操作#3將不會執行並被立即踢出隊列;
    • 對於操作#4來說,沒有任何依賴。所以它並發執行下載了#4。
ios-取消並發-demo

ios-取消並發-demo

如何深入?

在這個教程裡,我帶你入門了 iOS 並發的理論以及如何在 iOS 裡實現它。我為你做了一個關於並發的很好的介紹,解釋了 GCD,並且給你展示瞭如何創建串行和並行隊列。另外,我們還了解了 NSOperationQueues 。你現在應該很清楚全局中心調度和 NSOperationQueue 之間的區別。

要進一步了解 iOS 並發,我建議你閱讀蘋果的並髮指南

要是參考的話,你可以在 在Github的iOS並發庫 找到我們在這裡提到的完整源代碼。

文章譯自 appcoda iOS Concurrency: Getting Started with NSOperation and Dispatch Queues

本文由 落格博客 原創撰寫:落格博客 » iOS併發:入門 NSOperation 和 Dispatch Queues

轉載請保留出處和原文鏈接:https://www.logcg.com/archives/1436.html

關於作者

R0uter

如非聲明,本人所著文章均為原創手打,轉載請註明本頁面鏈接和我的名字。

發表評論

您的電子郵件地址不會被公開. 必填字段標 *