本文由SwiftGG獨家授權發布。
多核處理器是中央處理器(CPU)自出現以來最大的技術進步,這意味著它可以同時運行多條線程,并且可以在任何時刻處理多于一個的任務。
串行執行以及偽多線程都已經是多年以前的歷史了,如果你不是年輕到沒有使用過老式的電腦,又或者你有機會去接觸搭載著舊操作系統的舊電腦,你就能輕易明白我的話。但是,不管 CPU 擁有多少個核心,不管它有多么強大,開發者如果不好好利用這些優勢 ,那就沒有任何意義。這時就需要使用到多線程以及多任務編程了。開發者不僅可以,實際上是必需要好好利用設備上 CPU 的多線程能力,這就需要開發者將程序分解為多個部分,并讓它們在多個線程中并發執行。
并發編程有很多好處,但是最明顯的優勢包括用更少的時間完成所需的任務,防止界面卡頓,更佳的用戶體驗,等等。想像一下,如果應用需要在主線程下載一堆圖片,那種體驗有多糟糕,界面會一直卡頓直到所有的下載任務完成;用戶是絕對不接受這種應用的。
在 iOS 當中,蘋果提供了兩種方式進行多任務編程:Grand Central Dispatch (GCD) 和 NSOperationQueue 框架。當我們需要把任務分配到不同的線程中,或者是非主隊列的其它隊列中時,這兩種方法都可以很好地滿足需求。選擇哪一種方法是很主觀的行為,但是本教程只關注前一種,即GCD。不管使用哪一種方法,有一條規則必須要牢記:任何操作都不能堵塞主線程,必須使其用于界面響應以及用戶交互。所有的耗時操作或者對 CPU 需求大的任務都要在并發或者后臺隊列中執行。對于新手來說,這可能有點理解和實踐,但這就是我們所需要做的。
GCD 是在 iOS 4 中推出的,它為并發、性能以及并行任務提供了很大的靈活性和選擇性。但是在 Swift 3 之前,它有一個很大的劣勢:由于它的編程風格很接近底層的 C,與 Swift 的編程風格差別很大,它的 API 很難記,即使是在 Objective-C 當中使用也很不方便。這就是很多開發都避免使用 GCD 而選擇 NSOperationQueue 的主要原因。簡單地進行一下搜索,你就能了解 GCD 曾經的語法是怎么樣的。
這些在 Swift 3 當中有了很大的變化。Swift 3 采用了全新的 Swift 語法風格改寫了 GCD,這讓開發都可以很輕松地上手。而這些變化讓我有了動力來寫這篇文章,這里主要介紹了 Swift 3 當中 GCD 最基礎也最重要的知識。如果你曾經使用過舊語法風格的 GCD(即使只用過一點),那么這里介紹的新風格對你來說就是小菜一碟;如果你之前沒有使用過 GCD,那你就即將開啟一段編程的新篇章。
在正式開始討論今天的主題前,我們需要先了解一些更具體的概念。首先,GCD 中的核心詞是 dispatch queue。一個隊列實際上就是一系列的代碼塊,這些代碼可以在主線程或后臺線程中以同步或者異步的方式執行。一旦隊列創建完成,操作系統就接管了這個隊列,并將其分配到任意一個核心中進行處理。不管有多少個隊列,它們都能被系統正確地管理,這些都不需要開發者進行手動管理。隊列遵循 FIFO 模式(先進先出),這意味著先進隊列的任務會先被執行(想像在柜臺前排隊的隊伍,排在第一個的會首先被服務,排在最后的就會最后被服務)。我們會在后面的第一個例子中更清楚地理解這個概念。
接下來,另一個重要的概念就是 WorkItem(任務項)。一個任務項就是一個代碼塊,它可以隨同隊列的創建一起被創建,也可以被封裝起來,然后在之后的代碼中進行復用。正如你所想的,任務項的代碼就是 dispatch queue 將會執行的代碼。隊列中的任務項也是遵循 FIFO 模式。這些執行可以是同步的,也可以是異步的。對于同步的情況下,應用會一直堵塞當前線程,直到這段代碼執行完成。而當異步執行的時候,應用先執行任務項,不等待執行結束,立即返回。我們會在后面的實例里看到它們的區別。
了解完這兩個概念(隊列和任務項)之后,我們需要知道一個隊列可以是串行或并發的。在串行隊列中,一個任務項只有在前一個任務項完成后才能執行(除非它是第一個任務項),而在并發隊列中,所有的任務項都可以并行執行。
在為主隊列添加任務時,無論何時都要加倍小心。這個隊列要隨時用于界面響應以及用戶交互。與此相關的還有另一個規則,所有與用戶界面相關的更新都必須在主線程執行。如果你嘗試在后臺線程更新 UI,系統并不保證這個更新何時會發生,大多數情況下,這會都用戶帶來不好的體驗。但是,所有在發生在界面更新前的任務都可以在后臺線程執行。舉例來說,我們可以在從隊列,或者后臺隊列中下載圖片數據,然后在主線程中更新對應的 image view。
我們不一定需要每次都創建自己的隊列。系統維護的全局隊列可以用來執行任何我們想執行的任務。至于隊列全在哪一個線程運行,iOS 維護了一個線程池,即一系列除主線程之外的線程,系統會從中挑選一至多條線程來使用(取決于你所創建的隊列的數據,以及隊列創建的方式)。哪一條線程會被使用,對于開發者來說是未知的,而是由系統根據當前的并發任務,處理器的負載等情況來進行“決定”。講真,除了系統,誰又想去處理上述的這些工作呢。
我們的測試環境.
在本文中,接下來我們會使用幾個小的,具體的示例來介紹 GCD 的概念。正常情況下,我們使用 Playground 來演示就可以了,并不需要創建一個 demo 應用,但是我們沒辦法使用 Playground 來演示 GCD 的示例。因為在 Playground 當中無法使用不同的線程來調用函數,盡管我們的一些示例是可以在上面運行的,但并不是全部。因此,我們使用一個正常的工程來進行演示,以克服所有可能碰到的潛在問題,你可以在這里下載項目并打開。
這個工程幾乎是空的,除了下述額外的兩點:
- 在 ViewController.swift 文件中,我們可以看到一系列未實現的方法。每一個方法中,我們都將演示一個 GCD 的特性,你要做的事情就是在在
viewDidAppear(_:)
中去除相應方法調用的注釋,讓對應的方法被調用 。 - 在 Main.storyboard 中,ViewController 控制器添加了一個 imageView,并且它的 IBOutlet 屬性已經被正確地連接到 ViewController 類當中。稍后我們將會使用這個 imageView 來演示一個真實的案例。
現在讓我們開始吧。
認識 Dispatch Queue
在 Swift 3 當中,創建一個 dispatch queue 的最簡單方式如下:
let queue = DispatchQueue(label: "com.appcoda.myqueue")
你唯一要做的事就是為你的隊列提供一個獨一無二的標簽(label)。使用一個反向的 DNS 符號("com.appcoda.myqueue")就很好,因為用它很容易創造一個獨一無二的標簽,甚至連蘋果公司都是這樣建議的。盡管如此,這并不是強制性的,你可以使用你喜歡的任何字符串,只要這個字符串是唯一的。除此之外,上面的構造方法并不是創建隊列的唯一方式。在初始化隊列的時候可以提供更多的參數,我們會在后面的篇幅中談論到它。
一旦隊列被創建后,我們就可以使用它來執行代碼了,可以使用 sync
方法來進行同步執行,或者使用 async
方法來進行異步執行。因為我們剛開始,所以先使用代碼塊(一個閉包)來作為被執行的代碼。在后面的篇幅中,我們會初始化并使用 dispatch 任務項(DispatchWorkItem) 來取代代碼塊(需要注意的是,對于隊列來說代碼塊也算是一個任務項)。我們先從同步執行開始,下面要做的就是打印出數字 0~9 :
使用紅點可以讓我們更容易在控制臺輸出中識別出打印的內容,特別是當我們后面添加更多的隊列執行的時候
將上述代碼段復制粘貼到 ViewController.swift
文件中的 simpleQueues()
方法內。確保這個方法在 ViewDidAppear(_:)
里沒有被注釋掉,然后執行。觀察 Xcode 控制臺,你會看到輸出并沒有什么特別的。我們看到控制臺輸出了一些數字,但是這些數字沒有辦法幫我們做出關于 GCD 特性的任何結論。接下來,更新simpleQueues()
方法內的代碼,在為隊列添加閉包的代碼后面增加另一段代碼。這段代碼用于輸出數字 100 ~ 109(僅用于區別數字不同):
for i in 100..<110 {
print("??", i)
}
上面的這個 for 循環會在主隊列運行,而第一個會在后臺線程運行。程序的運行會在隊列的 block 中止,并且直到隊列的任務結束前,它都不會執行主線程的徨,打印數字 100 ~ 109。程序會有這樣的行為,是因為我們使用了同步執行。你也可以在控制臺中看到輸出結果:
但是如果我們使用 async
方法運行代碼塊會發生什么事呢?在這種情況下,程序不需要等待隊列任務完成才往下執行,它會立馬返回主線程,然后第二個 for 循環會與隊列里的循環同時運行。在我們看到會發生什么事之前,將隊列的執行改用 async
方法:
現在,執行代碼,并查看 Xcode 的控制臺:
對比同步執行,這次的結果有趣多了。我們看到主隊列中的代碼(第二個 for 循環)和 dispatch queue 里面的代碼并行運行了。在這里,這個自定義隊列在一開始的時候獲得了更多的執行時間,但是這只是跟優先級有關(這我們將在文章后面學習到)。這里想要強調的是,當另外一個任務在后臺執行的時候,主隊列是處于空閑狀態的,隨時可以執行別的任務,而同步執行的隊列是不會出現這種情況的。
盡管上面的示例很簡單,但已經清楚地展示了一個程序在同步隊列與異步隊列中行為的差異。我們將在接下來的示例中繼續使用這種彩色的控制臺輸出,請記住,特定顏色代碼特定隊列的運行結果,不同的顏色代表不同的隊列。
Quality Of Service (QOS) 和優先級
在使用 GCD 與 dispatch queue 時,我們經常需要告訴系統,應用程序中的哪些任務比較重要,需要更高的優先級去執行。當然,由于主隊列總是用來處理 UI 以及界面的響應,所以在主線程執行的任務永遠都有最高的優先級。不管在哪種情況下,只要告訴系統必要的信息,iOS 就會根據你的需求安排好隊列的優先級以及它們所需要的資源(比如說所需的 CPU 執行時間)。雖然所有的任務最終都會完成,但是,重要的區別在于哪些任務更快完成,哪些任務完成得更晚。
用于指定任務重要程度以及優先級的信息,在 GCD 中被稱為 Quality of Service (Qos)。事實上,Qos 是有幾個特定值的枚舉類型,我們可以根據需要的優先級,使用合適的 Qos 值來初始化隊列。如果沒有指定 Qos,則隊列會使用默認優先級進行初始化。要詳細了解 Qos 可用的值,可以參考這個文檔,請確保你仔細看過這個文檔。下面的列表總結了 Qos 可用的值,它們也被稱為 Qos classes。第一個 class 代碼了最高的優先級,最后一個代表了最低的優先級:
- userInteractive
- userInitiated
- default
- utility
- background
- unspecified
現在回到我們的項目中,這次我們要使用 queueWithQos()
方法。先聲明和初始化下面兩個 dispatch queue:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.userInitiated)
注意,這里我們使用了相同的 Qos class,所以這兩個隊列擁有相同的運行優先級。就像我們之前所做的一樣,第一個隊列會執行一個循環并打印出 0 ~ 9(加上前面的紅點)。第二個隊列會執行另一個打印出 100 ~ 109 的循環(使用藍點)。
看到運行結果,我們可以確認這兩個隊列確實擁有相同的優先級(相同的 Qos class)—— 不要忘記在
viewDidAppear(_:)
中關閉 queueWithQos()
方法的注釋:從上面的截圖當中可以輕易看出這兩個任務被“均勻”地執行,而這也是我們預期的結果。現在讓我們把 queue2
的 Qos class 設置為 utility
(低優先級),如下所示:
let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.utility)
現在看看會發生什么:
毫無疑問地,第一個 dispatch queue(queue1)比第二個執行得更快,因為它的優先級比較高。即使 queue2
在第一個隊列執行的時候也獲得了執行的機會,但由于第一個隊列的優先級比較高,所以系統把多數的資源都分配給了它,只有當它結束后,系統才會去關心第二個隊列。
現在讓我們再做另外一個試驗,這次將第一個 queue 的 Qos class 設置為 background
:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.background)
這個優先級幾乎是最低的,現在運行代碼,看看會發生什么:
這次第二個隊列完成得比較早,因為 utility
的優先級比較 background
來得高。
通過上述的例子,我們已經清楚了 Qos 是如何運行的,但是如果我們在同時在主隊列執行任務的話會怎么樣呢?現在在方法的末尾加入下列的代碼:
for i in 1000..<1010 {
print("??", i)
}
同時,將第一個隊列的 Qos class 設置為更高的優先級:
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
下面是運行結果:
我們再次看到了主隊列默認擁有更高的優先級,queue1
與主列隊是并行執行的。而 queue2
是最后完成的,并且妝其它兩個隊列在執行的時候,它沒有得到太多執行的機會,因為它的優先級是最低的。
并發隊列
到目前為止,我們已經看到了 dispatch queue 分別在同步與異步下的運行情況,以及操作系統如何根據 Qos class 來影響隊列的優先級的。但是在前面的例子當中,我們都是將隊列設置為串行(serial)的。這意味著,如果我們向隊列中加入超過一個的任務,這些任務將會被一個接一個地依次執行,而非同時執行。接下來,我們將學習如何使多個任務同時執行,換句話說,我們將學習如何使用并發(concurrent)隊列。
在項目中,這次我們會使用 concurrentQueue()
方法(請在 viewDidAppear(_:))
方法中將對應的代碼取消注釋)。在這個新方法中,創建如下的新隊列:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility)
現在,將如下的任務(或者對應的任務項)添加到隊列中:
當這段代碼執行的時候,這些任務會被以串行的方式執行。這可以在下面的截圖上看得很清楚:
接下來,我們修改下 anotherQueue
隊列的初始化方式:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .concurrent)
在上面的初始化當中,有一個新的參數:attributes
。當這個參數被指定為 concurrent
時,該特定隊列中的所有任務都會被同時執行。如果沒有指定這個參數,則隊列會被設置為串行隊列。事實上,Qos 參數也不是必須的,在上面的初始化中,即使我們將這些參數去掉也不會有任何問題。
現在重新運行代碼,可以看到任務都被并行地執行了:
注意,改變 Qos class 也會影響程序的運行。但是,只要在初始化隊列的時候指定了 concurrent
,這些任務就會以并發的方式運行,并且它們各自都會擁有運行時間。
這個 attributes
參數也可以接受另一個名為 initiallyInactive
的值。如果使用這個值,任務不會被自動執行,而是需要開發者手動去觸發。我們接下來會進行說明,但是在這之前,需要對代碼進行一些改動。首先,聲明一個名為 inactiveQueue
的成員屬性,如下所示:
var inactiveQueue: DispatchQueue!
現在,初始化隊列,并將其賦值給 inactiveQueue
:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .initiallyInactive)
inactiveQueue = anotherQueue
使用成員屬性是有必要的,因為 anotherQueue
是在 concurrentQueues()
方法中定義的,只在該方法中可用。當它退出這個方法的時候,應用程序將無法使用這個變量,我們也無法激活這個隊列,最重要的是,可能會造成運行時崩潰。
現在重新運行程序,可以看到控制臺沒有任何的輸出,這正是我們預期的。現在可以在 viewDidAppear(_:)
方法中添加如下的代碼:
if let queue = inactiveQueue {
queue.activate()
}
DispatchQueue
類的 activate()
方法會讓任務開始執行。注意,這個隊列并沒有被指定為并發隊列,因此它們會以串行的方式執行:
現在的問題是,我們如何在指定 initiallyInactive
的同時將隊列指定為并發隊列?其實很簡單,我們可以將兩個值放入一個數組當中,作為 attributes
的參數,替代原本指定的單一數值:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .userInitiated, attributes: [.concurrent, .initiallyInactive])
延遲執行
有時候,程序需要對代碼塊里面的任務項進行延時操作。GCD 允許開發者通過調用一個方法來指定某個任務在延遲特定的時間后再執行。
這次我們將代碼寫在 queueWithDelay()
方法內,這個方法也在初始項目中定義好了。我們會從添加如下代碼開始:
let delayQueue = DispatchQueue(label: "com.appcoda.delayqueue", qos: .userInitiated)
print(Date())
let additionalTime: DispatchTimeInterval = .seconds(2)
一開始,我們像通常一樣創建了一個 DispatchQueue
,這個隊列會在下一步中被使用到。接著,我們打印了當前時間,之后這個時間將會被用來驗證執行任務的延遲時間,最后我們指定了延遲時間。延遲時間通常是一個 DispatchTimeInterval
類型的枚舉值(在內部它被表示為整型值),這個值會被添加到 DispatchTime
中用于指定延遲時間。在這個示例中,設定的等待執行時間是兩秒。這里我們使用的是 seconds
方法,除此之外,還有以下的方法可以使用:
- microseconds
- milliseconds
- nanoseconds
現在開始使用這個隊列:
delayQueue.asyncAfter(deadline: .now() + additionalTime) {
print(Date())
}
now()
方法返回當前的時間,然后我們額外把需要延遲的時間添加進來。現在運行程序,控制臺將會打印出如下的輸出:
的確,dispatch queue 中的任務在兩秒后被執行了。除此之外,我們還有別的方法可以用來指定執行時間。如果不想使用任務預定義的方法,你可以直接使用一個 Double
類型的值添加到當前時間上:
delayQueue.asyncAfter(deadline: .now() + 0.75) {
print(Date())
}
在這個情況下,任務會被延遲 0.75 秒后執行。也可以不使用 now()
方法,這樣一來,我們就必須手動指定一個值作為 DispatchTime
的參數。上面演示的只是一個延遲執行的最簡單方法,但實際上你也不大需要別的方法了。
訪問主隊列和全局隊列
在前面的所有例子當中,我們都手動創建了要使用的 dispatch queue。實際上,我們并不總是需要自己手動創建,特別是當我們不需要改變隊列的優先級的時候。就像我在文章一開頭講過的,操作系統會創建一個后臺隊列的集合,也被稱為全局隊列(global queue)。你可以像使用自己創建的隊列一樣來使用它們,只是要注意不能濫用。
訪問全局隊列十分簡單:
let globalQueue = DispatchQueue.global()
可以像我們之前使用過的隊列一樣來使用它:
當使用全局隊列的時候,并沒有太多的屬性可供我們進行修改。但是,你仍然可以指定你想要使用隊列的 Quality of Service:
let globalQueue = DispatchQueue.global(qos: .userInitiated)
如果沒有指定 Qos class(就像本節的第一個示例),就會默認以 default
作為默認值。
無論你使不使用全局隊列,你都不可避免地要經常訪問主隊列,大多數情況下是作為更新 UI 而使用。在其它隊列中訪問主隊列的方法也非常簡單,就如下面的代碼片段所示,并且需要在調用的同時指定同步還是異步執行:
DispatchQueue.main.async {
// Do something
}
事實上,你可以輸入 DispatchQueue.main.
來查看主隊列的所有可用選項,Xcode 會通過自動補全來顯示主隊列所有可用的方法,不過上面代碼展示的就是我們絕大多數時間會用到的(事實上,這個方法是通用的,對于所有隊列,都可以通過輸入 . 之后讓 Xcode 來進行自動補全)。就像上一節所做的一樣,你也可以為代碼的執行增加延時。
現在讓我們來看一個真實的案例,演示如何通過主隊列來更新 UI。在初始工程的 Main.storyboard
文件中有一個 ViewController
場景(sence),這個 ViewController
場景包含了一個 imageView,并且這個 imageView 已經通過 IBOutlet
連接到對應的 ViewController
類文件中。在這里,我們通過 fetchImage()
方法(目前是空的)來下載一個 Appcoda 的 logo 并將其展示到 imageView 當中。下面的代碼完成了上述動作(我不會在這里針對 URLSession 做相關的討論,以及介紹它如何使用):
func fetchImage() {
let imageURL: URL = URL(string: "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png")!
(URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: imageURL, completionHandler: { (imageData, response, error) in
if let data = imageData {
print("Did download image data")
self.imageView.image = UIImage(data: data)
}
}).resume()
}
注意,我們并沒有在主隊列更新 UI 界面,而是試圖在 dataTask(...)
方法的 completion handler 里運行的后臺線程來更新界面。編譯、運行程序,看看會發生什么(不要忘記調用 fetchImage()
方法):
即使我們得到了圖片下載完成的信息,但是沒有看到圖片被顯示到 imageView 上面,這是因為 UI 并沒有更新。大多數情況下,這個圖片會在信息出現的一小會后顯示出來(但是如果其他任務也在應用程序中執行,上述情況不保證會發生),問題不僅如此,你還會在控制臺看到關于在后臺線程更新 UI 的一大串出錯信息。
現在,讓我們改正這段有問題的行為,使用主隊列來更新用戶界面。在編輯上述方法的時候,只需要改動底下所示部分,并注意我們是如何使用主隊列的:
if let data = imageData {
print("Did download image data")
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
再次運行程序,會看到圖片在下載完成后被正確地顯示出來。主隊列確實被調用并更新了 UI。
使用 DispatchWorkItem 對象
DispatchWorkItem
是一個代碼塊,它可以在任意一個隊列上被調用,因此它里面的代碼可以在后臺運行,也可以在主線程運行。它的使用真的很簡單,就是一堆可以直接調用的代碼,而不用像之前一樣每次都寫一個代碼塊。
下面展示了使用任務項最簡單的方法:
let workItem = DispatchWorkItem {
// Do something
}
現在讓我們通過一個小例子來看看 DispatchWorkItem
如何使用。前往 useWorkItem()
方法,并添加如下代碼:
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
}
這個任務項的目的是將變量 value
的值增加 5。我們使用任務項對象去調用 perform()
方法,如下所示:
workItem.perform()
這行代碼會在主線程上面調用任務項,但是你也可以使用其它隊列來執行它。參考下面的示例:
let queue = DispatchQueue.global()
queue.async {
workItem.perform()
}
這段代碼也可以運行得很好。但是,有一個更快地方法可以達到同樣的效果。DispatchQueue
類為此目的提供了一個便利的方法:
queue.async(execute: workItem)
當一個任務項被調用后,你可以通知主隊列(或者任何其它你想要的隊列),如下所示:
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
上面的代碼會在控制臺打印出 value
變量的值,并且它是在任務項被執行的時候打印的。現在將所有代碼放到一起,userWorkItem()
方法內的代碼如下所示:
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
workItem.perform()
let queue = DispatchQueue.global(qos: .utility)
queue.async(execute: workItem)
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
}
下面是你運行程序后會看到的輸出(記得在 viewDidAppear(_:)
方法中調用上面的方法):
總結
這篇文章中提到的知識足夠你應付大多數情況下的多任務和并發編程了。但是,請記住,還有其它我們沒有提到的 GCD 概念,或者文章有提到但是沒有深入討論的概念。目的是想讓本篇文章對所有層次的開發者都簡單易讀。如果你之前沒有使用過 GCD,請認真考慮并嘗試一下,讓主隊列從繁重的任務中解脫出來。如果有可以在后臺線程執行的任務,讓將其移到后臺運行。在任何情況下,使用 GCD 都不困難,并且它能獲得的正面結果就是讓應用響應更快。開始享受 GCD 的樂趣吧!
可以在這個 Github 里找到本文使用的完整項目。