作者: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):
- 在
ViewController.swift
文件中,我們可以看到一系列未實(shí)現(xiàn)的方法畴蹭。每一個(gè)方法中坦仍,我們都將演示一個(gè) GCD 的特性,你要做的事情就是在在viewDidAppear(_:)
中去除相應(yīng)方法調(diào)用的注釋叨襟,讓對(duì)應(yīng)的方法被調(diào)用 繁扎。 - 在
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瓮增。