iOS 並行程式設計: 初探 NSOperation 和 Dispatch Queues

並行程式設計永遠是 iOS 開發中的重要內容。同時也是開發者們必須極力避免的「深水區」。如果你對它沒有一個深刻的理解,那它對於你來說確實很危險。未知的東西總是被認為是危險的。想像一下人們在生活中碰到的各種危險,有多少是真正的危險?


一旦人們真正了解了這些危險,這些所謂的危險其實不值一提。並行程式設計是一柄雙刃劍,你必須學會如何正確地使用和掌握它。它能讓你編寫出高效、快速和響應式的 App,但同時,如果使用不當,它會給你的 App 帶來一場災難。所以,在我們開始編寫任何並行程式代碼之前,首先來思考一下:你為什麼需要並行程式設計?以及你應該使用哪個 API 來解決問題?在 iOS 中,我們可以使用不同的 API。本教程將介紹其中兩個最常用的 API ── NSOperationDispatch Queue

為什麼需要並行程式設計?

假設你擁有豐富的 iOS 編程經驗。但不管你要創建的是何種類型的 App,你都應該知道並行編程能讓你的 App 跑得更快和更加具有響應式風格。這裡,我來介紹幾個關於學習和使用並行編程的好處:

  • 充分利用 iOS 設備的硬件性能:
    現在所有的 iOS 設備都具有多個內核,這就允許開發者以並行的方式同時執行多個任務。你應當充分利用這一特點來享受硬件所帶來的便利。
  • 用戶體驗更佳:
    你可能曾經寫過調用網絡服務的代碼,以處理諸如 IO 請求、大規模計算的任務。那麼你應該知道,在執行這些操作時,UI 線程會被「凍住」,App 將停止響應。每當用戶遇到這種情況,他們第一反應就是毫不猶豫地關閉你的 App 進程。而通過並行編程,這些任務可以放到後臺執行而不會阻塞主線程或者影響用戶操作。在處理繁重的數據加載操作的同時,他們仍然能夠點擊按鈕,滾動視圖或者在 App 中切換窗口。
  • 並行編程中的某些 API,比如 NSOperation 和 dispatch queue 使用起來非常簡單:
    創建、管理線程不是一件輕鬆的活計。這就是為什麼大部份開發者一聽到並行編程和多線程代碼就發怵的原因。 但是 iOS 擁有強大、簡單的並行 API,它們會令你徹底放鬆。你根本無需和任何線程的創建或低級 API 打交道。並行 API 會自動為你完成這些工作。使用這些 API 的另外一個好處,是它們能讓你輕易實現同步以避免競爭條件。當多個線程同時訪問一個共享資源時,往往會導致競爭條件出現,同時得到錯誤的結果。通過同步機制,我們可以保護好線程之間的共享資源。

關於並行程式設計,你需要知道什麼?

在本教程中,我們將向你詳細介紹關於並行編程的所有必要知識,同時解除你對它的所有疑慮。首先我們建議你先看一下關於塊(Swift 閉包)的內容,因為塊在並行 API 中使用得非常普遍。然後再來看我們對 dispatch queue 和 NSOperationQueue 的介紹。我們將逐一介紹這兩個 API 的概念,它們的區別,以及如何使用它們。

第一部份: GCD (Grand Central Dispatch)

GCD 是最接近於操作系統 Unix 底層的,最常見的處理並行代碼和執行異步操作的 API。GCD 負責創建和管理任務隊列。首先讓我們來了解什麼是隊列。

隊列是什麼?

隊列是這樣一種數據結構,它以先進先出(FIFO)的方式來管理所存儲的對象。隊列就像是人們在電影院售票窗口進行排隊,票總是先賣給先到的人。位於隊列前面的人要比位於後面的其他人先買到電影票。在計算機學中的隊列與此類似,第一個添加到隊列中的對象,也是第一個從隊列中移除的對象。

queue-line-2-1166050-1280x960

圖片來源: FreeImages.com/Sigurd Decroos

Dispatch 隊列

Dispatch 隊列是一種在 App 中執行異步任務和並行任務的方法。它是這樣一種隊列,App 將任務以塊(代碼塊)的方式提交給它。有兩種不同的 Dispatch 隊列:(1)串行隊列,(2)並行隊列。在介紹兩者的區別前,你需要首先理解一點,分配給這兩種隊列的任務,其實是在另一個單獨的線程中執行的,而不是在創建它們的線程中執行的。也就是說,你在主線程中創建了一個代碼塊並將之提交給 Dispatch 隊列,但所有的任務(代碼塊)都不在主線程而是在另外的線程中運行的。

串行隊列

當創建一個串行隊列時,這個隊列一次只能執行一個任務。在同一個串行隊列中的任務會和平共處並以串行的方式執行。但是並沒有規定所有任務都只能在一個串行隊列中執行,你仍然可以通過使用多個串行隊列的方式來並行地執行任務。舉個例子,你可以同时創建兩個串行隊列,雖然每個隊列一次只執行一個任務,但總體效果就是每次有兩個任務在異步地執行。

串行隊列非常利於處理共享資源。它能保證對共享資源的訪問是以串行的方式進行的並有效避免了競爭條件。想像一下,只有一個售票窗口,卻有一堆想買電影票的人,那麼坐在售票窗口處的那個售票員就是一個共享資源。如果這個售票員同時為這麼多人提供服務,那麼可以預料現場將是何等的混亂。要解決這個問題,人們需要排隊(串行隊列),那麼售票員一次只需要給一個人賣票就可以了。

但是,並不是說電影院一次只能賣給一張票給顧客。如果增加兩個售票窗口,電影院就可以一次向三個人賣票。這就是我所說的,你仍然可以使用多個串行隊列並行地執行多個任務。

使用串行隊列的好處包括:

  1. 保證對共享資源的順序訪問,避免競爭條件
  2. 任務以預定的順序執行。當你向串行隊列提交任務后,任務將以他們加入的順序執行。
  3. 你可以創建多個串行隊列。

並行隊列

顧名思義,並行隊列允許你以並行的方式執行多個任務。這些任務(代碼塊)一開始的順序是它們加入時的順序,但它們執行時都是並行的,它們不需要等待其它任務。並行隊列只能保證任務一開始的順序是加入時的順序,但無法知道它們的執行順序、執行時間或者某一時刻被執行的任務數。

例如,你提交了三個任務(任務 #1,#2,#3)到同一個並行隊列。這些任務會以並行的方式執行,它們啟動的順序就是它們加入隊列的順序。但是它們的執行時間和完成時間是不盡相同的。甚至可能任務 #2、#3 在 #1 之後啟動了,卻在任務 #1 之前完成。任務的執行由系統決定。

使用隊列

我們已經介紹了串行隊列和並行隊列,現在來看看如何使用它們。默認,系統會提供給每個 App 一個串行隊列和四個並行隊列。主 dispatch 隊列是全局的串行隊列,用於執行主線程中的任務。主 Dispatch 隊列通常用於更新 App 的用戶界面,並執行所有與 UIView 的顯示有關的任務。它一次只會執行一個任務,因此當你在主 Dispatch 隊列中運行繁重任務時,UI 會被阻塞。

除了主 Dispatch 隊列,系統還提供了四個並行隊列。我們稱之為全局 Dispatch 隊列。這些隊列在 App 範圍內是全局的,但它們的優先級各不相同。要使用這些隊列,你需要用 dispatch_get_global_queue 函數獲得一個指定類型隊列的引用,這個函數的第一個參數可以採用以下四個值:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

這些值分別指定了並行隊列的四個不同的優先級。HIGH 表示優先級最高,BACKGROUND 表示優先級最低。因此你需要根據任務的優先程度來指定要使用的隊列。注意,這些隊列同時也被蘋果的 API 所使用,因此這些隊列中並不僅僅只有你的自己的任務。

最後,你還可以創建任意數量的串行和並行隊列。對於並行隊列,我強烈建議你使用上述四個全局隊列就可以了,當然,你也可以創建另外的隊列。

GCD 速查表

現在,你已經基本了解了 Dispatch 隊列。為了便於參考,我列出一個簡單的速查表。這個速查表很簡單,但列表中包含了你應該掌握的所有與 GCD 有關的知識點。

gcd-cheatsheet

不錯吧?現在讓我們創建一個小小的 Demo,以示範 Dispatch 隊列的用法。我將演示如何使用 Dispatch 隊列來優化 App 的性能,並使 App 更加「響應式」。

示例項目

我們的起始項目非常簡單,只是顯示四個 Image View,每個 Image View 都會從 Web 上請求一張圖片。Web 請求放在了主線程中。為了演示 UI 在響應性能上所受的影響,我在這些 Image View 下面放了一個滑動條。現在下載並運行起始項目。點擊 Start 按鈕,開始下載圖片。在下載過程中,拖動滑動條。 你會發現滑動條根本無法拖動。

concurrency-demo

當你點擊 Start 按鈕后,圖片開始在主線程中下載。顯然,這種方式很不好並導致了 UI 停止響應。不幸的是,直到目前為止,仍然有一些 App 以這種方式在主線程中進行繁重的加載操作。接下來,我們將以 Dispatch 隊列來解決這個問題。

我們會先用並行隊列的方式然後再用串行隊列的方式解決這個問題。

使用並行 Dispatch 隊列

在 Xcode 中打開 ViewController.swift 文件。如果你看過代碼,你會發現一個叫 didClickOnStart 的 Action 方法。這個方法用於圖片的下載。在這個方法中我們是這樣做的:

每個 downloader 都是一個下載任務,同時所有任務目前都是在主線程中操作的。現在,我們從全局的並行隊列中獲得一個默認優先級的隊列。

首先獲得默認優先級的並行隊列,並將其引用到變量 dispatch_get_global_queue。在這代碼塊中,我們提交了一個任務用於下載第一個圖片。當圖片下載完成,又向主隊列中提交另一個任務用於將下載好的圖片顯示到 Image View 中。也就是說,我們將圖片下載任務放到了後台線程中,而將與 UI 有關的任務放到了主隊列中。

將剩下的圖片以同樣方式下載,代碼最終變成這樣:

我們將四張圖片的下載以並行方式提交到默認隊列中。現在運行程序,它會運行得更快(如果你遇到任何錯誤,檢查你的代碼是否和上述代碼一致)。注意在下載圖片的同時,你還可以流利地拖動滑動條。

使用串行 Dispatch 隊列

我們還可以用串行隊列來解決這個問題。依然是 ViewController.swift 文件中的 didClickOnStart() 方法。這次我們用串行隊列方式來下載圖片。在使用串行隊列的時候,我們需要特別注意你當前所引用的是哪一個串行隊列。每個 App 都會有一個默認的串行隊列,即 UI 更新所使用的主隊列。因此要注意,在使用串行隊列時,我們必須創建一個新的串行隊列,否則我們的任務會在 App 執行更新 UI 的時候執行你的任務。這將導致錯誤和卡頓感出現,導致用戶體驗變差。我們可以用 dispatch_queue_create 函數創建新隊列,並以前面的方式來提交任務。修改后的代碼如下所示:

如你所見,唯一不同的事情僅僅是把並行隊列換成了串行隊列。當再次運行程序,你會發現圖片在後台下載,你仍然可以和 UI 進行交互。

但有兩個地方需要注意:

  1. 相對於並行隊列來說,下載圖片的時間會有一定的延長。因為我們一次只會下載一張圖片。每個任務都得等上一個任務完成。
  2. 圖片下載的順序依序為 image1,image2,image3,image4。因為串行隊列一次只執行一個任務。

第二部份: Operation Queue

GCD 是一種底層的 C 語言 API,它允許開發者以並行方式執行任務。而 Operation 隊列相對來說是一種更高級和抽象的隊列模型,產生於 GCD 之上。換句話說,你可以像 GCD 一樣執行並行任務,但卻可以使用面向對象的方式。也就是說,Operation 隊列讓只會讓開發者更加輕鬆。

但與 GCD 不同,Operation 隊列不遵從先進先出的原則。這裡列出了二者的不同之處:

  1. 不遵循先進先出原則:在 Operation 隊列中,你可以為任務設定執行的優先級並為任務之間添加依賴性,也就是說,你可以讓一些任務總是在其它任務執行完之後再執行。這就是它們不需要遵循先進先出原則的原因。
  2. 默認,任務以並行方式執行:你無法將任務以串行方式執行,當然,你可以通過設置任務之間的依賴的方式,讓 Operation 隊列以某種順序執行任務。
  3. Operation 隊列是 NSOperationQueue 類的實例,它的任務就是作為 NSOperation 對象的容器。

NSOperation

準備提交給 Operation 隊列的任務必須以 NSOperation 實例的方式進行封裝。我們介紹過 GCD 的任務是以塊的形式進行提交。類似地,提交給 Operation 隊列的任務必須封裝在 NSOperation 對象中。你可以簡單地將 NSOperation 視作一個單獨的任務單元。

NSOperation 是一個抽象類,它無法直接使用,因此我們必須對它進行子類化。在 iOS SDK 中,提供了兩種 NSOperation 子類實現。這兩個類能夠直接使用,但你仍然可以直接對 NSOperation 進行子類化,創建自己的子類來執行任務。可以直接使用的兩個子類分別是:

  1. NSBlockOperation – 這個類用一個或多個塊創建。它可以包含不止一個塊,只有當全部塊的代碼都執行完才視作該任務完成。
  2. NSInvocationOperation – 這個類創建出的 NSOperation 可用於執行指定對象的選擇器(Selector)。

但使用 NSOperation 有什麼好處?

  1. 首先,它們可以用 NSOperation 類的 addDependency(op:NSOperation) 方法來添加依賴。當你需要依賴一個任務的執行結果來啟動另一個任務時,你就可以用 NSOperation 了。
  2. NSOperation Illustration

  3. 其次,你可以通過 queuePriority 屬性來改變一個 Operation 的優先級,該屬性可能的取值包括:

    高優先級的任務將優先執行。
  4. 你可以取消指定隊列的全部或單個 Operation。在將一個 Operation 加入隊列后你又可以取消它。在該 Operation 上調用 cancel() 方法即可取消該 Operation。在取消一個 Operation 時,會碰到三種情形:
    • 你的 Operation 已經完成。這種情形下, cancel 方法什麼也不做。
    • 你的 Operation 正在執行。在這種情形下,系統不會強行終止 Operation 中的代碼,但會將 cancelled 屬性設置為 true。
    • Operation 已經位於隊列中,處於等待執行狀態。在這種情形下,這個 Operation 不會被執行。
  5. NSOperation 有三個有用的布爾屬性,分別是 finished、cancelled 和 ready。當 Operation 被執行完,finished 就會設置為 true。當 Operation 被取消,cancelled 就被設置為 true。當 Operation 即將要被執行時,ready 就被設置為 true。
  6. 任何 NSOperation 對象都可設置一個可選的完成塊,當任務完成時會調用這個完成塊。當 finished 屬性一設置為 true 后,立即調用該完成塊。

現在,讓我們修改我們的代碼,讓它用 NSOperationQueue 來重新實現。首先在 ViewController 中聲明一個變量:

然後,將 didClickOnStart 方法修改為如下代碼,這段代碼中顯示了如何在 NSOperationQueue 中操作 Operation:

如上述代碼所示,我們使用 addOperationWithBlock 方法以指定的塊(或者叫做 Swift 閉包)來創建一個新的 Operation 實例。很簡單,是吧?要在主隊列中執行任務,我們用 NSOperationQueue 的 mainQueue() 方法代替 GCD 中的 dispatch_async() 方法來獲取主隊列,然後將要在主隊列中執行的操作提交給主隊列。

你可以運行程序來試一試。如果代碼正確, App 將在後台下載圖片,同時不會阻塞 UI。

在前面的例子中,我們使用 addOperationWithBlock 方法將 Operation 添加進隊列中。接下來我們看看如何使用 NSBlockOperation 來做同樣的事情。同時,我們可以擁有更多的選項和功能,比如為 Operation 設置一個完成塊。將 didClickOnStart 方法修改為:

對於每個 Operation,我們都創建了一個新的 NSBlockOperation 用於將要執行的任務封裝到塊中。通過 NSBlockOperation,我們還可以設置它的完成塊。當 Operation 執行完后,完成塊將被調用。為求簡便,我們僅僅在完成塊中輸出了一個簡單消息表示 Operation 執行完畢。如果你再次運行程序,你將看到控制台中輸出了如下內容:

取消 Operation

正如我們前面提到的,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 中新增如下方法:

當然,你需要在導航欄中新加入的 Cancel 按鈕和 didClickOnCancel 方法之間創建一個連接。這需要我們回到 Main.storyboard 並使用連接面板。在連接面板中,你將看到 Received Actions 一欄下面有一個未連接的 didSelectCancel() 方法。點擊 + 按鈕,從小圓圈拖一條線到 Cancel 按鈕。然後在 didClickOnStart 方法中增加如下語句以創建依賴:

然後修改 Operation #1 的完成塊中的日誌打印語句為:

同樣,修改 Operation #2、#3 和 #4 的日誌語句,以便我們能理解整個流程。現在運行程序。點擊 Start 按鈕后,立即點擊 Cancel 按鈕。這將在 Operation #1 執行完之後取消所有的 Operation。詳細步驟如下:

  • 因為 Operation #1 已經開始執行,取消動作將對它毫無影響。因此它的 cancelled 值打印出來就是 false,App 將如圖 1 所顯示。
  • 如果你點擊 Cancel 按鈕的速度夠快,Operation #2 就會被取消。cancelAllOperations() 方法將讓它停止執行,因此第二張圖片不會被下載。
  • Operation #3 已經在隊列中,等待 Operation #2 完成。因為它依賴於 Operation #2 的完成而 Operation #2 又被取消了,因此 Operation #3 不會被執行,並立即被移出隊列。
  • Operation #4 沒有設置依賴。它被異步執行,並下載了第四張圖片。

ios-concurrency-cancel-demo

接下來做什麼?

在本教程中,我完整地介紹了 iOS 並行程式設計的概念,以及如何在 iOS 中實現並行程式。我詳細地介紹了什麼是並行程式,GCD 以及如何創建串行隊列和並行隊列。同時我們也介紹了 NSOperationQueue。你也了解到了 GDC 和 NSOperationQueue 到底有什麼不同。

要進一步了解 iOS 並行程式設計,我建議你去讀 蘋果的並行編程指南

作為參考,你還可以從 Github 下載本教程中的完整原始碼

對本文有任何疑問,請留言。衷心希望你能對本文發表任何評論。

譯者簡介:楊宏焱,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年開始學習蘋果 iOS 開發,精通 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作,著有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》(合作翻譯)等。

原文iOS Concurrency: Getting Started with NSOperation and Dispatch Queues


軟體開發員並有多年iOS程式開發經驗。對教學充滿熱誠,樂意分享知識。著作有《Application Development with Swift》。歡迎在推特與Hossam聯絡。

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This