Swift 3 中的 GCD 與 Dispatch Queue

作者:GABRIEL THEODOROPOULOS偷拔,原文鏈接廓块,原文日期:2016-11-16
譯者:小鍋;校對(duì):saitjr买决;定稿:CMB

自中央處理器(CPU)出現(xiàn)以來(lái)促脉,最大的技術(shù)進(jìn)步當(dāng)屬多核處理器,這意味著它可以同時(shí)運(yùn)行多條線程策州,并且可以在任何時(shí)刻處理至少一個(gè)任務(wù)。

串行執(zhí)行以及偽多線程都已經(jīng)成為了歷史宫仗,如果你經(jīng)歷過(guò)老式電腦的時(shí)代够挂,又或者你接觸過(guò)搭載著舊操作系統(tǒng)的舊電腦,你就能輕易明白我的話藕夫。但是孽糖,不管 CPU 擁有多少個(gè)核心枯冈,不管它有多么強(qiáng)大,開(kāi)發(fā)者如果不好好利用這些優(yōu)勢(shì) 办悟,那就沒(méi)有任何意義尘奏。這時(shí)就需要使用到多線程以及多任務(wù)編程了。開(kāi)發(fā)者不僅可以病蛉,而且必須要好好利用設(shè)備上 CPU 的多線程能力炫加,這就需要開(kāi)發(fā)者將程序分解為多個(gè)部分,并讓它們?cè)诙鄠€(gè)線程中并發(fā)執(zhí)行铺然。

并發(fā)編程有很多好處俗孝,但是最明顯的優(yōu)勢(shì)包括用更少的時(shí)間完成所需的任務(wù),防止界面卡頓魄健,展現(xiàn)更佳的用戶體驗(yàn)赋铝,等等。想像一下沽瘦,如果應(yīng)用需要在主線程下載一堆圖片革骨,那種體驗(yàn)有多糟糕,界面會(huì)一直卡頓直到所有的下載任務(wù)完成析恋;用戶是絕對(duì)不接受這種應(yīng)用的良哲。

在 iOS 當(dāng)中,蘋(píng)果提供了兩種方式進(jìn)行多任務(wù)編程:Grand Central Dispatch (GCD)NSOperationQueue绿满。當(dāng)我們需要把任務(wù)分配到不同的線程中臂外,或者是非主隊(duì)列的其它隊(duì)列中時(shí),這兩種方法都可以很好地滿足需求喇颁。選擇哪一種方法是很主觀的行為漏健,但是本教程只關(guān)注前一種,即 GCD橘霎。不管使用哪一種方法蔫浆,有一條規(guī)則必須要牢記:任何操作都不能堵塞主線程,必須使其用于界面響應(yīng)以及用戶交互姐叁。所有的耗時(shí)操作或者對(duì) CPU 需求大的任務(wù)都要在并發(fā)或者后臺(tái)隊(duì)列中執(zhí)行瓦盛。對(duì)于新手來(lái)說(shuō),理解和實(shí)踐可能都會(huì)比較難外潜,這也正是這篇文章的意義所在原环。

GCD 是在 iOS 4 中推出的,它為并發(fā)处窥、性能以及并行任務(wù)提供了很大的靈活性和選擇性嘱吗。但是在 Swift 3 之前,它有一個(gè)很大的劣勢(shì):由于它的編程風(fēng)格很接近底層的 C滔驾,與 Swift 的編程風(fēng)格差別很大谒麦, API 很難記俄讹,即使是在 Objective-C 當(dāng)中使用也很不方便。這就是很多開(kāi)發(fā)都避免使用 GCD 而選擇 NSOperationQueue 的主要原因绕德。簡(jiǎn)單地百度一下患膛,你就能了解 GCD 曾經(jīng)的語(yǔ)法是怎么樣的。

Swift 3 中耻蛇,這些都有了很大的變化踪蹬。Swift 3 采用了全新的 Swift 語(yǔ)法風(fēng)格改寫(xiě)了 GCD,這讓開(kāi)發(fā)都可以很輕松地上手城丧。而這些變化讓我有了動(dòng)力來(lái)寫(xiě)這篇文章延曙,這里主要介紹了 Swift 3 當(dāng)中 GCD 最基礎(chǔ)也最重要的知識(shí)。如果你曾經(jīng)使用過(guò)舊語(yǔ)法風(fēng)格的 GCD(即使只用過(guò)一點(diǎn))亡哄,那么這里介紹的新風(fēng)格對(duì)你來(lái)說(shuō)就是小菜一碟枝缔;如果你之前沒(méi)有使用過(guò) GCD,那你就即將開(kāi)啟一段編程的新篇章蚊惯。

在正式開(kāi)始討論今天的主題前愿卸,我們需要先了解一些更具體的概念。首先截型,GCD 中的核心詞是 dispatch queue趴荸。一個(gè)隊(duì)列實(shí)際上就是一系列的代碼塊,這些代碼可以在主線程或后臺(tái)線程中以同步或者異步的方式執(zhí)行宦焦。一旦隊(duì)列創(chuàng)建完成发钝,操作系統(tǒng)就接管了這個(gè)隊(duì)列,并將其分配到任意一個(gè)核心中進(jìn)行處理波闹。不管有多少個(gè)隊(duì)列酝豪,它們都能被系統(tǒng)正確地管理,這些都不需要開(kāi)發(fā)者進(jìn)行手動(dòng)管理精堕。隊(duì)列遵循 FIFO 模式(先進(jìn)先出)孵淘,這意味著先進(jìn)隊(duì)列的任務(wù)會(huì)先被執(zhí)行(想像在柜臺(tái)前排隊(duì)的隊(duì)伍,排在第一個(gè)的會(huì)首先被服務(wù)歹篓,排在最后的就會(huì)最后被服務(wù))瘫证。我們會(huì)在后面的第一個(gè)例子中更清楚地理解這個(gè)概念。

接下來(lái)庄撮,另一個(gè)重要的概念就是 WorkItem(任務(wù)項(xiàng))背捌。一個(gè)任務(wù)項(xiàng)就是一個(gè)代碼塊,它可以隨同隊(duì)列的創(chuàng)建一起被創(chuàng)建洞斯,也可以被封裝起來(lái)载萌,然后在之后的代碼中進(jìn)行復(fù)用。正如你所想,任務(wù)項(xiàng)的代碼就是 dispatch queue 將會(huì)執(zhí)行的代碼扭仁。隊(duì)列中的任務(wù)項(xiàng)也是遵循 FIFO 模式。這些執(zhí)行可以是同步的厅翔,也可以是異步的乖坠。對(duì)于同步的情況下,應(yīng)用會(huì)一直堵塞當(dāng)前線程刀闷,直到這段代碼執(zhí)行完成熊泵。而當(dāng)異步執(zhí)行的時(shí)候,應(yīng)用先執(zhí)行任務(wù)項(xiàng)甸昏,不等待執(zhí)行結(jié)束顽分,立即返回。我們會(huì)在后面的實(shí)例里看到它們的區(qū)別施蜜。

了解完這兩個(gè)概念(隊(duì)列和任務(wù)項(xiàng))之后卒蘸,我們需要知道一個(gè)隊(duì)列可以是串行或并行的。在串行隊(duì)列中翻默,一個(gè)任務(wù)項(xiàng)只有在前一個(gè)任務(wù)項(xiàng)完成后才能執(zhí)行(除非它是第一個(gè)任務(wù)項(xiàng))缸沃,而在并行隊(duì)列中,所有的任務(wù)項(xiàng)都可以并行執(zhí)行修械。

在為主隊(duì)列添加任務(wù)時(shí)趾牧,無(wú)論何時(shí)都要加倍小心。這個(gè)隊(duì)列要隨時(shí)用于界面響應(yīng)以及用戶交互肯污。并且記住一點(diǎn)翘单,所有與用戶界面相關(guān)的更新都必須在主線程執(zhí)行。如果你嘗試在后臺(tái)線程更新 UI蹦渣,系統(tǒng)并不保證這個(gè)更新何時(shí)會(huì)發(fā)生哄芜,大多數(shù)情況下,這會(huì)都用戶帶來(lái)不好的體驗(yàn)剂桥。但是忠烛,所有發(fā)生在界面更新前的任務(wù)都可以在后臺(tái)線程執(zhí)行。舉例來(lái)說(shuō)权逗,我們可以在從隊(duì)列美尸,或者后臺(tái)隊(duì)列中下載圖片數(shù)據(jù),然后在主線程中更新對(duì)應(yīng)的 image view斟薇。

我們不一定需要每次都創(chuàng)建自己的隊(duì)列师坎。系統(tǒng)維護(hù)的全局隊(duì)列可以用來(lái)執(zhí)行任何我們想執(zhí)行的任務(wù)。至于隊(duì)列在哪一個(gè)線程運(yùn)行堪滨,iOS 維護(hù)了一個(gè)線程池胯陋,即一系列除主線程之外的線程,系統(tǒng)會(huì)從中挑選一至多條線程來(lái)使用(取決于你所創(chuàng)建的隊(duì)列的數(shù)據(jù),以及隊(duì)列創(chuàng)建的方式)遏乔。哪一條線程會(huì)被使用义矛,對(duì)于開(kāi)發(fā)者來(lái)說(shuō)是未知的,而是由系統(tǒng)根據(jù)當(dāng)前的并發(fā)任務(wù)盟萨,處理器的負(fù)載等情況來(lái)進(jìn)行“決定”凉翻。講真,除了系統(tǒng)捻激,誰(shuí)又想去處理上述的這些工作呢制轰。

我們的測(cè)試環(huán)境

在本文中,接下來(lái)我們會(huì)使用幾個(gè)小的胞谭,具體的示例來(lái)介紹 GCD 的概念垃杖。正常情況下,我們使用 Playground 來(lái)演示就可以了丈屹,并不需要?jiǎng)?chuàng)建一個(gè) demo 應(yīng)用调俘,但是我們沒(méi)辦法使用 Playground 來(lái)演示 GCD 的示例。因?yàn)樵?Playground 當(dāng)中無(wú)法使用不同的線程來(lái)調(diào)用函數(shù)泉瞻,盡管我們的一些示例是可以在上面運(yùn)行的脉漏,但并不是全部。因此袖牙,我們使用一個(gè)正常的工程來(lái)進(jìn)行演示侧巨,以克服所有可能碰到的潛在問(wèn)題,你可以在這里下載項(xiàng)目并打開(kāi)鞭达。

這個(gè)工程幾乎是空的司忱,除了下述額外的兩點(diǎn):

  1. ViewController.swift 文件中,我們可以看到一系列未實(shí)現(xiàn)的方法畴蹭。每一個(gè)方法中坦仍,我們都將演示一個(gè) GCD 的特性,你要做的事情就是在在 viewDidAppear(_:) 中去除相應(yīng)方法調(diào)用的注釋叨襟,讓對(duì)應(yīng)的方法被調(diào)用 繁扎。
  2. Main.storyboard 中,ViewController 控制器添加了一個(gè) imageView糊闽,并且它的 IBOutlet 屬性已經(jīng)被正確地連接到 ViewController 類(lèi)當(dāng)中梳玫。稍后我們將會(huì)使用這個(gè) imageView 來(lái)演示一個(gè)真實(shí)的案例。

現(xiàn)在讓我們開(kāi)始吧右犹。

認(rèn)識(shí) Dispatch Queue

在 Swift 3 當(dāng)中提澎,創(chuàng)建一個(gè) dispatch queue 的最簡(jiǎn)單方式如下:

let queue = DispatchQueue(label: "com.appcoda.myqueue")

你唯一要做的事就是為你的隊(duì)列提供一個(gè)獨(dú)一無(wú)二的標(biāo)簽(label)。使用一個(gè)反向的 DNS 符號(hào)("com.appcoda.myqueue")就很好念链,因?yàn)橛盟苋菀讋?chuàng)造一個(gè)獨(dú)一無(wú)二的標(biāo)簽盼忌,甚至連蘋(píng)果公司都是這樣建議的积糯。盡管如此,這并不是強(qiáng)制性的谦纱,你可以使用你喜歡的任何字符串看成,只要這個(gè)字符串是唯一的。除此之外服协,上面的構(gòu)造方法并不是創(chuàng)建隊(duì)列的唯一方式绍昂。在初始化隊(duì)列的時(shí)候可以提供更多的參數(shù),我們會(huì)在后面的篇幅中談?wù)摰剿?/p>

一旦隊(duì)列被創(chuàng)建后偿荷,我們就可以使用它來(lái)執(zhí)行代碼了,可以使用 sync 方法來(lái)進(jìn)行同步執(zhí)行唠椭,或者使用 async 方法來(lái)進(jìn)行異步執(zhí)行跳纳。因?yàn)槲覀儎傞_(kāi)始,所以先使用代碼塊(一個(gè)閉包)來(lái)作為被執(zhí)行的代碼贪嫂。在后面的篇幅中寺庄,我們會(huì)初始化并使用 dispatch 任務(wù)項(xiàng)(DispatchWorkItem)來(lái)取代代碼塊(需要注意的是,對(duì)于隊(duì)列來(lái)說(shuō)代碼塊也算是一個(gè)任務(wù)項(xiàng))力崇。我們先從同步執(zhí)行開(kāi)始斗塘,下面要做的就是打印出數(shù)字 0~9 :

使用紅點(diǎn)可以讓我們更容易在控制臺(tái)輸出中識(shí)別出打印的內(nèi)容,特別是當(dāng)我們后面添加更多的隊(duì)列執(zhí)行的時(shí)候

將上述代碼段復(fù)制粘貼到 ViewController.swift 文件中的 simpleQueues() 方法內(nèi)亮靴。確保這個(gè)方法在 ViewDidAppear(_:) 里沒(méi)有被注釋掉馍盟,然后執(zhí)行。觀察 Xcode 控制臺(tái)茧吊,你會(huì)看到輸出并沒(méi)有什么特別的贞岭。我們看到控制臺(tái)輸出了一些數(shù)字,但是這些數(shù)字沒(méi)有辦法幫我們做出關(guān)于 GCD 特性的任何結(jié)論搓侄。接下來(lái)瞄桨,更新 simpleQueues() 方法內(nèi)的代碼,在為隊(duì)列添加閉包的代碼后面增加另一段代碼讶踪。這段代碼用于輸出數(shù)字 100 ~ 109(僅用于區(qū)別數(shù)字不同):

for i in 100..<110 {
    print("??", i)
}

上面的這個(gè) for 循環(huán)會(huì)在主隊(duì)列運(yùn)行芯侥,而第一個(gè)會(huì)在后臺(tái)線程運(yùn)行。程序的運(yùn)行會(huì)在隊(duì)列的 block 中止乳讥,并且直到隊(duì)列的任務(wù)結(jié)束前柱查,它都不會(huì)執(zhí)行主線程,也不會(huì)打印數(shù)字 100 ~ 109雏婶。程序會(huì)有這樣的行為物赶,是因?yàn)槲覀兪褂昧送綀?zhí)行。你也可以在控制臺(tái)中看到輸出結(jié)果:

但是如果我們使用 async 方法運(yùn)行代碼塊會(huì)發(fā)生什么事呢留晚?在這種情況下酵紫,程序不需要等待隊(duì)列任務(wù)完成才往下執(zhí)行告嘲,它會(huì)立馬返回主線程,然后第二個(gè) for 循環(huán)會(huì)與隊(duì)列里的循環(huán)同時(shí)運(yùn)行奖地。在我們看到會(huì)發(fā)生什么事之前橄唬,將隊(duì)列的執(zhí)行改用 async 方法:

現(xiàn)在,執(zhí)行代碼参歹,并查看 Xcode 的控制臺(tái):

對(duì)比同步執(zhí)行仰楚,這次的結(jié)果有趣多了。我們看到主隊(duì)列中的代碼(第二個(gè) for 循環(huán))和 dispatch queue 里面的代碼并行運(yùn)行了犬庇。在這里僧界,這個(gè)自定義隊(duì)列在一開(kāi)始的時(shí)候獲得了更多的執(zhí)行時(shí)間,但是這只是跟優(yōu)先級(jí)有關(guān)(這我們將在文章后面學(xué)習(xí)到)臭挽。這里想要強(qiáng)調(diào)的是捂襟,當(dāng)另外一個(gè)任務(wù)在后臺(tái)執(zhí)行的時(shí)候,主隊(duì)列是處于空閑狀態(tài)的欢峰,隨時(shí)可以執(zhí)行別的任務(wù)葬荷,而同步執(zhí)行的隊(duì)列是不會(huì)出現(xiàn)這種情況的。

盡管上面的示例很簡(jiǎn)單纽帖,但已經(jīng)清楚地展示了一個(gè)程序在同步隊(duì)列與異步隊(duì)列中行為的差異宠漩。我們將在接下來(lái)的示例中繼續(xù)使用這種彩色的控制臺(tái)輸出,請(qǐng)記住懊直,特定顏色代碼特定隊(duì)列的運(yùn)行結(jié)果扒吁,不同的顏色代表不同的隊(duì)列。

Quality Of Service(QoS)和優(yōu)先級(jí)

在使用 GCD 與 dispatch queue 時(shí)吹截,我們經(jīng)常需要告訴系統(tǒng)瘦陈,應(yīng)用程序中的哪些任務(wù)比較重要,需要更高的優(yōu)先級(jí)去執(zhí)行波俄。當(dāng)然晨逝,由于主隊(duì)列總是用來(lái)處理 UI 以及界面的響應(yīng),所以在主線程執(zhí)行的任務(wù)永遠(yuǎn)都有最高的優(yōu)先級(jí)懦铺。不管在哪種情況下捉貌,只要告訴系統(tǒng)必要的信息,iOS 就會(huì)根據(jù)你的需求安排好隊(duì)列的優(yōu)先級(jí)以及它們所需要的資源(比如說(shuō)所需的 CPU 執(zhí)行時(shí)間)冬念。雖然所有的任務(wù)最終都會(huì)完成趁窃,但是,重要的區(qū)別在于哪些任務(wù)更快完成急前,哪些任務(wù)完成得更晚醒陆。

用于指定任務(wù)重要程度以及優(yōu)先級(jí)的信息,在 GCD 中被稱為 Quality of Service(QoS)裆针。事實(shí)上刨摩,QoS 是有幾個(gè)特定值的枚舉類(lèi)型寺晌,我們可以根據(jù)需要的優(yōu)先級(jí),使用合適的 QoS 值來(lái)初始化隊(duì)列澡刹。如果沒(méi)有指定 QoS呻征,則隊(duì)列會(huì)使用默認(rèn)優(yōu)先級(jí)進(jìn)行初始化。要詳細(xì)了解 QoS 可用的值罢浇,可以參考這個(gè)文檔陆赋,請(qǐng)確保你仔細(xì)看過(guò)這個(gè)文檔。下面的列表總結(jié)了 Qos 可用的值嚷闭,它們也被稱為 QoS classes攒岛。第一個(gè) class 代碼了最高的優(yōu)先級(jí),最后一個(gè)代表了最低的優(yōu)先級(jí):

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified

現(xiàn)在回到我們的項(xiàng)目中胞锰,這次我們要使用 queueWithQos() 方法阵子。先聲明和初始化下面兩個(gè) dispatch queue:

let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.userInitiated)

注意,這里我們使用了相同的 QoS class,所以這兩個(gè)隊(duì)列擁有相同的運(yùn)行優(yōu)先級(jí)。就像我們之前所做的一樣矾麻,第一個(gè)隊(duì)列會(huì)執(zhí)行一個(gè)循環(huán)并打印出 0 ~ 9(加上前面的紅點(diǎn))窍奋。第二個(gè)隊(duì)列會(huì)執(zhí)行另一個(gè)打印出 100 ~ 109 的循環(huán)(使用藍(lán)點(diǎn))。

看到運(yùn)行結(jié)果暖璧,我們可以確認(rèn)這兩個(gè)隊(duì)列確實(shí)擁有相同的優(yōu)先級(jí)(相同的 QoS class)—— 不要忘記在 viewDidAppear(_:) 中關(guān)閉 queueWithQos() 方法的注釋:

從上面的截圖當(dāng)中可以輕易看出這兩個(gè)任務(wù)被“均勻”地執(zhí)行案怯,而這也是我們預(yù)期的結(jié)果。現(xiàn)在讓我們把 queue2 的 QoS class 設(shè)置為 utility(低優(yōu)先級(jí))澎办,如下所示:

let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.utility)

現(xiàn)在看看會(huì)發(fā)生什么:

毫無(wú)疑問(wèn)地嘲碱,第一個(gè) dispatch queue(queue1)比第二個(gè)執(zhí)行得更快,因?yàn)樗膬?yōu)先級(jí)比較高局蚀。即使 queue2 在第一個(gè)隊(duì)列執(zhí)行的時(shí)候也獲得了執(zhí)行的機(jī)會(huì)麦锯,但由于第一個(gè)隊(duì)列的優(yōu)先級(jí)比較高,所以系統(tǒng)把多數(shù)的資源都分配給了它琅绅,只有當(dāng)它結(jié)束后扶欣,系統(tǒng)才會(huì)去關(guān)心第二個(gè)隊(duì)列。

現(xiàn)在讓我們?cè)僮隽硗庖粋€(gè)試驗(yàn)千扶,這次將第一個(gè) queue 的 QoS class 設(shè)置為 background

let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.background)

這個(gè)優(yōu)先級(jí)幾乎是最低的料祠,現(xiàn)在運(yùn)行代碼,看看會(huì)發(fā)生什么:

這次第二個(gè)隊(duì)列完成得比較早澎羞,因?yàn)?utility 的優(yōu)先級(jí)比較 background 來(lái)得高髓绽。

通過(guò)上述的例子,我們已經(jīng)清楚了 QoS 是如何運(yùn)行的妆绞,但是如果我們?cè)谕瑫r(shí)在主隊(duì)列執(zhí)行任務(wù)的話會(huì)怎么樣呢顺呕?現(xiàn)在在方法的末尾加入下列的代碼:

for i in 1000..<1010 {
    print("??", i)
}

同時(shí)枫攀,將第一個(gè)隊(duì)列的 QoS class 設(shè)置為更高的優(yōu)先級(jí):

let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)

下面是運(yùn)行結(jié)果:

我們?cè)俅慰吹搅酥麝?duì)列默認(rèn)擁有更高的優(yōu)先級(jí),queue1 與主列隊(duì)是并行執(zhí)行的塘匣。而 queue2 是最后完成的脓豪,并且?jiàn)y其它兩個(gè)隊(duì)列在執(zhí)行的時(shí)候,它沒(méi)有得到太多執(zhí)行的機(jī)會(huì)忌卤,因?yàn)樗膬?yōu)先級(jí)是最低的扫夜。

并行隊(duì)列

到目前為止,我們已經(jīng)看到了 dispatch queue 分別在同步與異步下的運(yùn)行情況驰徊,以及操作系統(tǒng)如何根據(jù) QoS class 來(lái)影響隊(duì)列的優(yōu)先級(jí)的笤闯。但是在前面的例子當(dāng)中,我們都是將隊(duì)列設(shè)置為串行(serial)的棍厂。這意味著颗味,如果我們向隊(duì)列中加入超過(guò)一個(gè)的任務(wù),這些任務(wù)將會(huì)被一個(gè)接一個(gè)地依次執(zhí)行牺弹,而非同時(shí)執(zhí)行浦马。接下來(lái),我們將學(xué)習(xí)如何使多個(gè)任務(wù)同時(shí)執(zhí)行张漂,換句話說(shuō)晶默,我們將學(xué)習(xí)如何使用并行(concurrent)隊(duì)列。

在項(xiàng)目中航攒,這次我們會(huì)使用 concurrentQueue() 方法(請(qǐng)?jiān)?viewDidAppear(_:) 方法中將對(duì)應(yīng)的代碼取消注釋)磺陡。在這個(gè)新方法中,創(chuàng)建如下的新隊(duì)列:

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility)

現(xiàn)在漠畜,將如下的任務(wù)(或者對(duì)應(yīng)的任務(wù)項(xiàng))添加到隊(duì)列中:

當(dāng)這段代碼執(zhí)行的時(shí)候币他,這些任務(wù)會(huì)被以串行的方式執(zhí)行。這可以在下面的截圖上看得很清楚:

接下來(lái)憔狞,我們修改下 anotherQueue 隊(duì)列的初始化方式:

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .concurrent)

在上面的初始化當(dāng)中蝴悉,有一個(gè)新的參數(shù):attributes。當(dāng)這個(gè)參數(shù)被指定為 concurrent 時(shí)躯喇,該特定隊(duì)列中的所有任務(wù)都會(huì)被同時(shí)執(zhí)行辫封。如果沒(méi)有指定這個(gè)參數(shù),則隊(duì)列會(huì)被設(shè)置為串行隊(duì)列廉丽。事實(shí)上倦微,QoS 參數(shù)也不是必須的,在上面的初始化中正压,即使我們將這些參數(shù)去掉也不會(huì)有任何問(wèn)題欣福。

現(xiàn)在重新運(yùn)行代碼,可以看到任務(wù)都被并行地執(zhí)行了:

注意焦履,改變 QoS class 也會(huì)影響程序的運(yùn)行拓劝。但是雏逾,只要在初始化隊(duì)列的時(shí)候指定了 concurrent,這些任務(wù)就會(huì)以并行的方式運(yùn)行郑临,并且它們各自都會(huì)擁有運(yùn)行時(shí)間栖博。

這個(gè) attributes 參數(shù)也可以接受另一個(gè)名為 initiallyInactive 的值。如果使用這個(gè)值厢洞,任務(wù)不會(huì)被自動(dòng)執(zhí)行仇让,而是需要開(kāi)發(fā)者手動(dòng)去觸發(fā)。我們接下來(lái)會(huì)進(jìn)行說(shuō)明躺翻,但是在這之前丧叽,需要對(duì)代碼進(jìn)行一些改動(dòng)。首先公你,聲明一個(gè)名為 inactiveQueue 的成員屬性踊淳,如下所示:

var inactiveQueue: DispatchQueue!

現(xiàn)在,初始化隊(duì)列陕靠,并將其賦值給 inactiveQueue

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .utility, attributes: .initiallyInactive)
inactiveQueue = anotherQueue

使用成員屬性是有必要的迂尝,因?yàn)?anotherQueue 是在 concurrentQueues() 方法中定義的,只在該方法中可用剪芥。當(dāng)它退出這個(gè)方法的時(shí)候雹舀,應(yīng)用程序?qū)o(wú)法使用這個(gè)變量,我們也無(wú)法激活這個(gè)隊(duì)列粗俱,最重要的是,可能會(huì)造成運(yùn)行時(shí)崩潰虚吟。

現(xiàn)在重新運(yùn)行程序寸认,可以看到控制臺(tái)沒(méi)有任何的輸出,這正是我們預(yù)期的〈浚現(xiàn)在可以在 viewDidAppear(_:) 方法中添加如下的代碼:

if let queue = inactiveQueue {
    queue.activate()
}

DispatchQueue 類(lèi)的 activate() 方法會(huì)讓任務(wù)開(kāi)始執(zhí)行偏塞。注意,這個(gè)隊(duì)列并沒(méi)有被指定為并行隊(duì)列邦鲫,因此它們會(huì)以串行的方式執(zhí)行:

現(xiàn)在的問(wèn)題是灸叼,我們?nèi)绾卧谥付?initiallyInactive 的同時(shí)將隊(duì)列指定為并行隊(duì)列?其實(shí)很簡(jiǎn)單庆捺,我們可以將兩個(gè)值放入一個(gè)數(shù)組當(dāng)中古今,作為 attributes 的參數(shù),替代原本指定的單一數(shù)值:

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue", qos: .userInitiated, attributes: [.concurrent, .initiallyInactive])

延遲執(zhí)行

有時(shí)候滔以,程序需要對(duì)代碼塊里面的任務(wù)項(xiàng)進(jìn)行延時(shí)操作捉腥。GCD 允許開(kāi)發(fā)者通過(guò)調(diào)用一個(gè)方法來(lái)指定某個(gè)任務(wù)在延遲特定的時(shí)間后再執(zhí)行。

這次我們將代碼寫(xiě)在 queueWithDelay() 方法內(nèi)你画,這個(gè)方法也在初始項(xiàng)目中定義好了抵碟。我們會(huì)從添加如下代碼開(kāi)始:

let delayQueue = DispatchQueue(label: "com.appcoda.delayqueue", qos: .userInitiated)

print(Date())

let additionalTime: DispatchTimeInterval = .seconds(2)

一開(kāi)始桃漾,我們像通常一樣創(chuàng)建了一個(gè) DispatchQueue,這個(gè)隊(duì)列會(huì)在下一步中被使用到拟逮。接著撬统,我們打印了當(dāng)前時(shí)間,之后這個(gè)時(shí)間將會(huì)被用來(lái)驗(yàn)證執(zhí)行任務(wù)的延遲時(shí)間敦迄,最后我們指定了延遲時(shí)間恋追。延遲時(shí)間通常是一個(gè) DispatchTimeInterval 類(lèi)型的枚舉值(在內(nèi)部它被表示為整型值),這個(gè)值會(huì)被添加到 DispatchTime 中用于指定延遲時(shí)間颅崩。在這個(gè)示例中几于,設(shè)定的等待執(zhí)行時(shí)間是兩秒。這里我們使用的是 seconds 方法沿后,除此之外沿彭,還有以下的方法可以使用:

  • microseconds
  • milliseconds
  • nanoseconds

現(xiàn)在開(kāi)始使用這個(gè)隊(duì)列:

delayQueue.asyncAfter(deadline: .now() + additionalTime) {
    print(Date())
}

now() 方法返回當(dāng)前的時(shí)間,然后我們額外把需要延遲的時(shí)間添加進(jìn)來(lái)〖夤觯現(xiàn)在運(yùn)行程序喉刘,控制臺(tái)將會(huì)打印出如下的輸出:

的確,dispatch queue 中的任務(wù)在兩秒后被執(zhí)行了漆弄。除此之外睦裳,我們還有別的方法可以用來(lái)指定執(zhí)行時(shí)間。如果不想使用任務(wù)預(yù)定義的方法撼唾,你可以直接使用一個(gè) Double 類(lèi)型的值添加到當(dāng)前時(shí)間上:

delayQueue.asyncAfter(deadline: .now() + 0.75) {
    print(Date())
}

在這個(gè)情況下廉邑,任務(wù)會(huì)被延遲 0.75 秒后執(zhí)行。也可以不使用 now() 方法倒谷,這樣一來(lái)蛛蒙,我們就必須手動(dòng)指定一個(gè)值作為 DispatchTime 的參數(shù)。上面演示的只是一個(gè)延遲執(zhí)行的最簡(jiǎn)單方法渤愁,但實(shí)際上你也不大需要?jiǎng)e的方法了牵祟。

訪問(wèn)主隊(duì)列和全局隊(duì)列

在前面的所有例子當(dāng)中,我們都手動(dòng)創(chuàng)建了要使用的 dispatch queue抖格。實(shí)際上诺苹,我們并不總是需要自己手動(dòng)創(chuàng)建,特別是當(dāng)我們不需要改變隊(duì)列的優(yōu)先級(jí)的時(shí)候雹拄。就像我在文章一開(kāi)頭講過(guò)的收奔,操作系統(tǒng)會(huì)創(chuàng)建一個(gè)后臺(tái)隊(duì)列的集合,也被稱為全局隊(duì)列(global queue)滓玖。你可以像使用自己創(chuàng)建的隊(duì)列一樣來(lái)使用它們筹淫,只是要注意不能濫用。

訪問(wèn)全局隊(duì)列十分簡(jiǎn)單:

let globalQueue = DispatchQueue.global()

可以像我們之前使用過(guò)的隊(duì)列一樣來(lái)使用它:

當(dāng)使用全局隊(duì)列的時(shí)候,并沒(méi)有太多的屬性可供我們進(jìn)行修改损姜。但是饰剥,你仍然可以指定你想要使用隊(duì)列的 Quality of Service:

let globalQueue = DispatchQueue.global(qos: .userInitiated)

如果沒(méi)有指定 QoS class(就像本節(jié)的第一個(gè)示例),就會(huì)默認(rèn)以 default 作為默認(rèn)值摧阅。

無(wú)論你使不使用全局隊(duì)列汰蓉,你都不可避免地要經(jīng)常訪問(wèn)主隊(duì)列,大多數(shù)情況下是作為更新 UI 而使用棒卷。在其它隊(duì)列中訪問(wèn)主隊(duì)列的方法也非常簡(jiǎn)單顾孽,就如下面的代碼片段所示,并且需要在調(diào)用的同時(shí)指定同步還是異步執(zhí)行:

DispatchQueue.main.async {
    // Do something
}

事實(shí)上比规,你可以輸入 DispatchQueue.main. 來(lái)查看主隊(duì)列的所有可用選項(xiàng)若厚,Xcode 會(huì)通過(guò)自動(dòng)補(bǔ)全來(lái)顯示主隊(duì)列所有可用的方法,不過(guò)上面代碼展示的就是我們絕大多數(shù)時(shí)間會(huì)用到的(事實(shí)上蜒什,這個(gè)方法是通用的测秸,對(duì)于所有隊(duì)列,都可以通過(guò)輸入 . 之后讓 Xcode 來(lái)進(jìn)行自動(dòng)補(bǔ)全)灾常。就像上一節(jié)所做的一樣霎冯,你也可以為代碼的執(zhí)行增加延時(shí)。

現(xiàn)在讓我們來(lái)看一個(gè)真實(shí)的案例钞瀑,演示如何通過(guò)主隊(duì)列來(lái)更新 UI沈撞。在初始工程的 Main.storyboard 文件中有一個(gè) ViewController 場(chǎng)景(sence),這個(gè) ViewController 場(chǎng)景包含了一個(gè) imageView雕什,并且這個(gè) imageView 已經(jīng)通過(guò) IBOutlet 連接到對(duì)應(yīng)的 ViewController 類(lèi)文件中缠俺。在這里,我們通過(guò) fetchImage() 方法(目前是空的)來(lái)下載一個(gè) Appcoda 的 logo 并將其展示到 imageView 當(dāng)中贷岸。下面的代碼完成了上述動(dòng)作(我不會(huì)在這里針對(duì) URLSession 做相關(guān)的討論晋修,以及介紹它如何使用):

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()
}

注意,我們并沒(méi)有在主隊(duì)列更新 UI 界面凰盔,而是試圖在 dataTask(...) 方法的 completion handler 里運(yùn)行的后臺(tái)線程來(lái)更新界面。編譯倦春、運(yùn)行程序户敬,看看會(huì)發(fā)生什么(不要忘記調(diào)用 fetchImage() 方法):

即使我們得到了圖片下載完成的信息,但是沒(méi)有看到圖片被顯示到 imageView 上面睁本,這是因?yàn)?UI 并沒(méi)有更新尿庐。大多數(shù)情況下,這個(gè)圖片會(huì)在信息出現(xiàn)的一小會(huì)后顯示出來(lái)(但是如果其他任務(wù)也在應(yīng)用程序中執(zhí)行呢堰,上述情況不保證會(huì)發(fā)生)抄瑟,不僅如此,你還會(huì)在控制臺(tái)看到關(guān)于在后臺(tái)線程更新 UI 的一大串出錯(cuò)信息枉疼。

現(xiàn)在皮假,讓我們改正這段有問(wèn)題的行為鞋拟,使用主隊(duì)列來(lái)更新用戶界面。在編輯上述方法的時(shí)候惹资,只需要改動(dòng)底下所示部分贺纲,并注意我們是如何使用主隊(duì)列的:

if let data = imageData {
    print("Did download image data")

    DispatchQueue.main.async {
        self.imageView.image = UIImage(data: data)
    }
}

再次運(yùn)行程序,會(huì)看到圖片在下載完成后被正確地顯示出來(lái)褪测。主隊(duì)列確實(shí)被調(diào)用并更新了 UI猴誊。

使用 DispatchWorkItem 對(duì)象

DispatchWorkItem 是一個(gè)代碼塊,它可以在任意一個(gè)隊(duì)列上被調(diào)用侮措,因此它里面的代碼可以在后臺(tái)運(yùn)行懈叹,也可以在主線程運(yùn)行。它的使用真的很簡(jiǎn)單分扎,就是一堆可以直接調(diào)用的代碼澄成,而不用像之前一樣每次都寫(xiě)一個(gè)代碼塊。

下面展示了使用任務(wù)項(xiàng)最簡(jiǎn)單的方法:

let workItem = DispatchWorkItem {
    // Do something
}

現(xiàn)在讓我們通過(guò)一個(gè)小例子來(lái)看看 DispatchWorkItem 如何使用笆包。前往 useWorkItem() 方法环揽,并添加如下代碼:

func useWorkItem() {
    var value = 10

    let workItem = DispatchWorkItem {
        value += 5
    }
}

這個(gè)任務(wù)項(xiàng)的目的是將變量 value 的值增加 5。我們使用任務(wù)項(xiàng)對(duì)象去調(diào)用 perform() 方法庵佣,如下所示:

workItem.perform()

這行代碼會(huì)在主線程上面調(diào)用任務(wù)項(xiàng)歉胶,但是你也可以使用其它隊(duì)列來(lái)執(zhí)行它。參考下面的示例:

let queue = DispatchQueue.global()
queue.async {
    workItem.perform()
}

這段代碼也可以正常運(yùn)行巴粪。但是通今,有一個(gè)更快地方法可以達(dá)到同樣的效果。DispatchQueue 類(lèi)為此目的提供了一個(gè)便利的方法:

queue.async(execute: workItem)

當(dāng)一個(gè)任務(wù)項(xiàng)被調(diào)用后肛根,你可以通知主隊(duì)列(或者任何其它你想要的隊(duì)列)辫塌,如下所示:

workItem.notify(queue: DispatchQueue.main) {
    print("value = ", value)
}

上面的代碼會(huì)在控制臺(tái)打印出 value 變量的值,并且它是在任務(wù)項(xiàng)被執(zhí)行的時(shí)候打印的∨烧埽現(xiàn)在將所有代碼放到一起臼氨,userWorkItem() 方法內(nèi)的代碼如下所示:

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)
    }
}

下面是你運(yùn)行程序后會(huì)看到的輸出(記得在 viewDidAppear(_:) 方法中調(diào)用上面的方法):

總結(jié)

這篇文章中提到的知識(shí)足夠你應(yīng)付大多數(shù)情況下的多任務(wù)和并發(fā)編程了。但是芭届,請(qǐng)記住储矩,還有其它我們沒(méi)有提到的 GCD 概念,或者文章有提到但是沒(méi)有深入討論的概念褂乍。目的是想讓本篇文章對(duì)所有層次的開(kāi)發(fā)者都簡(jiǎn)單易讀持隧。如果你之前沒(méi)有使用過(guò) GCD,請(qǐng)認(rèn)真考慮并嘗試一下逃片,讓主隊(duì)列從繁重的任務(wù)中解脫出來(lái)屡拨。如果有可以在后臺(tái)線程執(zhí)行的任務(wù),讓將其移到后臺(tái)運(yùn)行。在任何情況下呀狼,使用 GCD 都不困難裂允,并且它能獲得的正面結(jié)果就是讓?xiě)?yīng)用響應(yīng)更快。開(kāi)始享受 GCD 的樂(lè)趣吧赠潦!

可以在這個(gè) Github 里找到本文使用的完整項(xiàng)目叫胖。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)她奥,最新文章請(qǐng)?jiān)L問(wèn) http://swift.gg瓮增。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哩俭,隨后出現(xiàn)的幾起案子绷跑,更是在濱河造成了極大的恐慌,老刑警劉巖凡资,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砸捏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡隙赁,警方通過(guò)查閱死者的電腦和手機(jī)垦藏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)伞访,“玉大人掂骏,你說(shuō)我怎么就攤上這事『裰溃” “怎么了弟灼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)冒黑。 經(jīng)常有香客問(wèn)我田绑,道長(zhǎng),這世上最難降的妖魔是什么抡爹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任掩驱,我火速辦了婚禮,結(jié)果婚禮上冬竟,老公的妹妹穿的比我還像新娘欧穴。我一直安慰自己,他們只是感情好诱咏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著缴挖,像睡著了一般袋狞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天苟鸯,我揣著相機(jī)與錄音同蜻,去河邊找鬼。 笑死早处,一個(gè)胖子當(dāng)著我的面吹牛湾蔓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砌梆,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼默责,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了咸包?” 一聲冷哼從身側(cè)響起桃序,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烂瘫,沒(méi)想到半個(gè)月后媒熊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坟比,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年芦鳍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葛账。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柠衅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出注竿,到底是詐尸還是另有隱情茄茁,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布巩割,位于F島的核電站裙顽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宣谈。R本人自食惡果不足惜愈犹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闻丑。 院中可真熱鬧漩怎,春花似錦、人聲如沸嗦嗡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)侥祭。三九已至叁执,卻和暖如春茄厘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谈宛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工次哈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吆录。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓窑滞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親恢筝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哀卫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容