Swift 3使用GCD和DispatchQueues
中央處理單元(CPU)自存在以來最大的改進之一是包含了多核,并在多核上運行多線程,這意味著CPU在特定時刻可以同時運行多個任務。
只能串行執行任務或偽多線程多年前就已成為歷史,串行? 偽多線程是什么鬼? 如果你聽過這些, 說明你是個老臘肉并且有接觸過舊式電腦,或者你親自在舊式電腦上用過舊系統。但是不論
CPU有幾核或它有多強, 如果你不使用它這些優點的話一切都是毫無意義,正是意識到這點, 多任務處理和多線程編程慢慢進入人們的視野, 開發者不僅可以, 而且應該在實際開發中盡可
的將程序分成能夠在多個線程上并發執行的代碼塊來充分發揮CPU的多任務處理能力。
多核多線程編程好處有很多:更快的執行, 更好的用戶體驗, 界面更流程等等。 想象下你在`main thread`選擇下載多張圖片, 然后整個界面直接被卡死了, 60秒后終于下載完成, 然后你欣慰的看到了下載的圖片,這樣的應用還能讓人愉悅的玩耍么?
在iOS中,蘋果提供了兩種方法來進行多任務處理:`Grand Central Dispatch`(以下簡稱`GCD`)和`NSOperationQueue`框架。 當你需要在`非main thread`或`非 main queue`上運行相關任務時, 它倆都能非常完滿的完成任務。要用哪個,這個你自己來決定,本篇教程我們只探討`GCD`的用法。在開始之前我們先了解一條任意時刻都必須遵守的規則:**主線程用于界面更新和用戶交互,任何耗時或者耗CPU的任務必須在 concurrent queue 或者 background queue 上運行**。小白可能很難領會為什么要這么做,老鳥告訴你記住并遵守就好了。
`GCD`首次在iOS 4中引入,在實現并發,高性能和并發任務中表現出良好的靈活性和可配置性。但在`Swift3`之前它都跟天書一樣,與`swift`格格不入的古董C語言風格,晦澀難記的方法名都讓你望而卻步,碼農們寧愿用`NSOperaionQueue`都不用`GCD`, 稍微的搜索了解下你就會明白有多糟糕。
`Swift3`中`GCD`的用法發生了巨大變化,全新的`Swift`風格語法上手更簡單,正是這些變更驅使我寫下這篇教程來介紹`Swift3`下關于`GCD`的日常用法和注意事項,如果你用過舊語法的`GCD`(即使就使用了那么一點),那新語法你也一樣可以駕輕就熟。
在正式開始前,首先來了解下`GCD`的一些基本知識:
1. `dispatch queue`: 最基本也最重要,其實就是一堆在主線程(或后臺線程)上同步(或異步)來執行的代碼,一旦被創建出來,操作系統就開始接手管理,在CPU上分配時間片來執行隊列內的代碼。開發者沒法參與`queue`的管理。隊列采用`FIFO模式`(先進先出),意味著先加入的也會被先完成,可以想象下超市排隊買單, 隊伍前的總是最先買單出去,后續會有示例來詳細說明( 譯者說明:對于`serial queue`來說的確是這樣 )。
2. `work item`:一段代碼塊,可以在queue創建的時候添加,也可以單獨創建方便之后復用,你可以把它當成將要在`queue`上運行的一個代碼塊。`work items`也遵循著`FIFO模式`,也可以同步(或異步)執行,如果選擇同步的方式,運行中的程序直到代碼塊完成才會繼續之后的工作,相對的,如果選擇異步,運行中的程序在觸發了代碼塊就立刻返回了。后續也會有示例來詳細說明。
3. `serial`(串行)vs`concurrent`(并行):`serial`將會執行完一個任務才會開始下一個,`concurrent`觸發完一個就立即進入下一個,而不管它是否已完成。
如果你要在`main queue`上運行代碼,那你要提高警惕,不能讓代碼塊的運行占用了界面響應更新的時間片。重申下規則:**任何時候你都應該在主線程上更新UI**。如果你嘗試在其他線程更新,實際情況是你將不知道界面會不會更新或者什么時候被更新????。雖然如此, 在更新ui前,你還是可以將更新UI前的相關準備工作在后臺先運行完畢,例如你可以在`background queue`上下載圖片數據,完成后你回到主線程來顯示圖片。
大部分情況下,你不用自己創建`queue`,系統已經幫你創建好了一些常用的全局`queue`,你可以直接使用它們來處理你想要運行的任務,你可能會關心隊列將會在哪個線程上運行, 你是否能夠指定線程來執行代碼神馬的, 實際情況是除了創建、配置、使用`queue`其他你什么都做不了。 iOS管理著了一個線程池, 意味著除了主線程還有一堆其他線程存在著,系統將會選擇其中的一個或多個(依賴于你創建了多少個queue以及你創建queue的方式),具體選擇哪個開發者是不知道的,操作系統會根據當前并發的任務,處理器的負載情況來做具體決定,但你真的想自己去管理這些么?
測試環境
為了更好的展示GCD的使用,我們本打算建立多個很小但很針對的示范工程, playground本來是個非常完美的解決方案, 但是因為playground不支持從不同線程上調用方法,最后只好妥協使用了一個普通的項目工程,你可以在這里下載它[ https://github.com/appcoda/GCDSamples/raw/master/Starter_Project.zip ]它除了以下兩點,整個工程項目幾乎為空:
1. 在`ViewController.swift`文件中你將會發現一系列沒有被實現的方法,每個方法將會給我們演示一個GCD的特性,你需要做的就是在viewDidLoad(_:)方法中對代碼取消注釋以便執行相關的方法。
2. 在`Main.storyboard`, `ViewController`場景中你會發現添加了一個`UIImageView`,并且它已經通過`IBOutlet`和`ViewController`相關聯,之后我們會在一個模擬真實使用場景的demo中使用到它。
正式進入話題:
Dispatch Queues入門
在Swift3中,創建`dispatch queue`方式如下:
let queue = DispatchQueue(label: "com.appcoda.myqueue")
只需給`queue`提供一個唯一標簽,獲取唯一標簽的一個很有效的做法是將你的dns地址反序,如(“com.appcoda.myqueue”),這也是蘋果推薦的做法。但這不是硬性要求,你可以使用任何唯一的字符串作為標簽。 label 不是創建`queue`的唯一參數,在后續介紹相關內容時會逐一講到其他參數。
一旦隊列創建完成,我們就可以用它來運行代碼,你可以使用`sync`來同步運行或使用`async`來異步運行。 為了更簡潔的演示,我們會先使用block(閉包)來提供可執行代碼,后續會使用`DispatchWorkItem`對象來代替block(注意:block也可當成`Queue`里的一個`DispatchWorkItem`, 譯者補充: DispatchWorkItem 的初始方法: init(qos:, flags:, block:) , block作為它其中的一個初始參數)。這里我們只是簡單的在`queue`中同步的打印0~9
為了在控制臺清晰的區分結果,我們添加了一個紅點,在后續添加更多隊列或任務執行時,帶顏色的點可以幫助我們更好的區分不同`queue`里的任務。
將上述代碼片段放入你建立的初始項目的`ViewController.swift`文件的`simpleQueues()`方法中,確保這個方法在`viewDidAppear(_:)`中沒有被注釋,然后運行它,查看Xcode控制臺,你將會看到一堆數字被打印出來,單個queue輸出并不能幫助我們理解GCD是怎么工作,為了對比,我們在`simpleQueues()`方法的block后面添加另外一個打印100~109的代碼塊(僅僅為了展示區別)
作為對比,紅點代碼塊在`background Queue`上運行,而藍點代碼塊將會在`main queue`上運行,觀察運行結果你會發現程序被卡住了,直到紅點代碼塊執行執行完畢,`main queue`上的藍點代碼塊才會被繼續執行,之所以如此是因為`queue`中代碼是同步執行的,控制臺輸出如下:
如果使用`async`方法會發生神馬呢?它會讓`queue`中的代碼異步執行嗎? 如果是這樣的話,程序應該不會像上面結果那樣卡住而應該在`queue`內任務都執行完成前返回主線程上執行第二個`for`循環,使用`async`方法更新下`queue`的運行方式:
現在,來看下Xcode的控制臺
相比于`sync`執行結果,`async`顯示的更加有趣,你會發現`main queue`的代碼(第二個`for`循環)和我們`queue`在并發執行,自定義`queue`在開始得到了更多運行時間,但是這僅僅是因為優先級(后續會講)。 通過這個示例我們搞懂了幾件事:
1. 使用async主線程和后臺線程可以并行執行任務
2. 使用sync則只能串行執行任務, 當前線程被卡住直到串行任務完成才繼續
盡管示例非常簡單,但它完美的展示了`queue`中的代碼塊是怎么同步或異步執行的。 我們也會將在后續的示例中繼續采用上例中色彩繽紛的打印日志(特定的顏色代碼特定`queue`中代碼執行結果,不同顏色代表不同的`queue`).
服務等級(QoS)和優先級
在使用GCD和DispatchQueues的時候我們經常需要告知操作系統App里的哪些任務比其他任務更重要,哪些優先級更高。`main queue`通常被用來處理UI和響應操作, 它擁有很高的優先級。 實際使用場景中,通過向系統提供QoS信息,iOS會根據你的配置合理的處理優先級并提供所需資源(如CPU執行時間),雖然最終所有的任務都將完成,但優先級會讓一部分早完成、一部分晚完成。
確定任務重要和優先級的屬性被稱為GCD服務等級(GCD QoS),實際上,QoS是個基于具體場景的枚舉類型,在初始隊列時,可以提供合適的QoS參數來得到相應的權限,如果沒有指定QoS,那么初始方法會使用隊列提供的默認的QoS值,QoS所有可用選型可以查看,開始之前,建議你仔細閱讀,以下會概括的介紹下可用的QoS場景(QoS Case),或者稱為QoS等級(QoS classes),從前到后,優先級從高到低:
-
userInteractive
-
userInitiated
-
default
-
utility
-
background
-
unspecified
回到Xcode項目,找到`queuesWithQoS()`方法,定義并初始化以下兩個全新的`dispatch queues`:
let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.userInitiated)
let queue2 = DispatchQueue(label: "com.appcoda.queue2",qos: DispatchQoS.userInitiated)
注意,兩個隊列都使用相同的QoS等級,所以執行時它們擁有相同權限。 像之前做的那樣,第一個隊列包括一個`for`循環來顯示0~9(帶紅點),第二個隊列將執行另外一個`for`循環來顯示100~109(帶藍點)。
查看下擁有相同權限(相同QoS)的隊列執行結果
記得對viewDidAppear(_:)中的queuesWithQos()方法取消注釋
不難發現,兩個隊列的任務被"均勻"的執行了,這正是我們所期待的結果 現在,如下圖所示將`queue2`的QoS等級改為更低的 utility
let queue2 = DispatchQueue(label: "com.appcoda.queue2",qos: DispatchQoS.utility)
查看結果:
毫無懸念,優先級更高的`queue1`比`queue2`更快被執行,雖然在`queue1`運行的時候`queue2`得到一個運行的機會,系統還是將資源傾斜給了被標記為更重要的`queue1`,等`queue1`內的任務全部被執行完成,系統才開始全心全意服務于`queue2` 。
現在開始做另外一個實驗: 這次將第一個隊列QoS等級改為 background :
let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.background)
讓我們看下使用 background 這個次低的優先級會發生什么:
這次因為 utility QoS 比 background QoS 優先級更高,第二個隊列更快被完成。
通過上述例子,我們了解了QoS等級的使用方法,但是如果我們同時在主線程執行一個任務會發生什么? 在方法的后面添加以下代碼:
同時賦予第一個隊列更高的權限
let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.userInitiated)
運行結果如下:
實踐證明: `main queue`默認就有一個很高的權限( 譯者注:實際運行發現也只是utility ????),`queue1`和`main queue`并發執行,`queue2`在其他兩個隊列運行時沒有得到多少運行的機會,最后才完成 。
并發隊列(Concurrent Queues)
目前為止,我們已經看到dispatch queues是怎樣同步和異步工作以及服務等級是怎么影響系統服務的優先等級,可能你會發現,上述所有示例中所有的`queue`都是 串行執行 的(serial),意味著如果你分配多個任務給任意一個隊列,這些任務將會一個接一個被完成, 而不是同時完成。在這小節中,我們將會展示怎樣讓多任務(多工作項)同時運行,或者換種說法,怎么創建一個并發的隊列。
回到項目,找到`concurrentQueues()`方法(記得取消對該方法的注釋) ,在這個新方法里創建一個如下的新隊列:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility)
接下來,在隊列中分配任務(或者稱工作項):
結果如下所示,任務被順序的執行:
接下來,修改`anotherQueue`隊列初始化方法:
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility,attributes: .concurrent)
上述代碼中添加了一個新參數`attributes`,當這個參數被賦予`concurrent`時,這個隊列所有的任務將被同時執行。 如果你不加這個參數,那么隊列默認就是串行執行的(serial)。 QoS不是必填項,在初始的時候完全可以忽略它。
再次運行代碼,可以發現任務被高度并發執行了:
注意到更改Qos等級同樣可以影響到任務的執行,無論如何,只要你初始了一個并發隊列,那么并發執行的任務會被同等對待,所有任務都將獲得被執行的機會
`attrubites`參數可以選擇另外一個值:`initiallyInactive`。 通過定義為`initiallyInactive` 隊列任務不會自動開始執行,需要開發者主動去觸發。 為了展示這個特性,需要建一個`inactiveQueue `隊列:
var inactiveQueue: DispatchQueue!
初始化一個隊列,然后賦值給`inactiveQueue `
let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility,attributes: .initiallyInactive)
inactiveQueue = anotherQueue
為了讓`concurrentQueues()`這個方法外的其他方法也可以 這個示例中的`inactiveQueue `屬性非常必要,因為`anotherQueue`的作用域僅在方法中。 當方法執行完畢,它就對應用不可見了,根本不可能被方法外的其他方法來使用。( 譯者注:這里寫的不太好,如果對象可能為空, 應該果斷用optional,而不應該使用!再去判斷是否空, inactiveQueue 應該被聲明為optional類型:`var inactiveQueue: DispatchQueue?` )
再次運行應用,你將發現根本沒有輸出,這是我們所期待的結果,通過在`viewDidAppear(_:)`方法中添加一下代碼來觸發隊列的運行:
if let queue = inactiveQueue {
queue.activate()
}
DispatchQueue類的`activate()`讓任務執行,因為隊列沒有標記為并發,所以它將順序執行:
現在問題來了:,怎么創建一個既是初始不活躍又是并發的隊列? 很簡單,與之前提供`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`供后續代碼使用;然后我們打印當前時間用于對比延遲任務對比。 延遲時間通常是個添加在`DispatchTime`值后面的`DispatchTimeInterval ` 枚舉(值類型為整型) ,用于說明延遲詳情(后續會講到)。在示例中,任務設置為延遲兩秒執行,這里使用的是`seconds`方法,除了這個,還提供了其他時間選項可供選擇:
-
milliseconds (毫秒,千分之一秒)
-
microseconds (微秒,千分之一毫秒)
-
nanoseconds (納秒,千分之一微秒)
說明完畢,開始使用隊列:
delayqueue.asyncAfter(deadline: .now() + additionalTime) {
print(Date())
}
`now()`方法返回當前時間,在此基礎上添加我們想要延遲的時間,運行代碼,控制臺將輸出如下結果:
確實,正如預期那樣,隊列的任務2秒后才被執行。注意如果你不想使用一些預定義的方法來指定等待時間,你還有另外一種選擇,直接在當前時間后直接添加一個`Double`值:
delayqueue.asyncAfter(deadline: .now() + 0.75) {
print(Date())
}
在這種情況下,任務將在0.75秒后被執行。 你可以不適用`now()`方法,但是你必須自行提供一個 DispatchTime 值,以上展示的就是在隊列中實現最簡單的延遲任務,實際上這也是你所需要掌握的延遲實現方法的全部內容。
訪問主隊列和全局隊列
前面的例子的`queue`都是通過手工創建得到,實際使用中并不總要你自己來做這個,特別是你不想改變隊列的屬性,在博文開始的地方我提到了系統會創建一系列的被稱為全局隊列的后臺隊列。 你可以像自己創建的隊列一樣使用它們,只要不要太濫用就行。
訪問一個全局隊列方法如下:
let globalQueue = Dispatchqueue.global()
你可以像我們之前使用自定義隊列那樣使用它:
使用全局變量,你只能使用部分屬性,例如服務等級(Qos class)
let globalQueue = Dispatchqueue.global(qos: .userInitiated)
如果你不指定該參數(像第一個用例那樣),那么Qos屬相將采用默認的`default`值
拋開全局隊列,你毋庸置疑會經常使用到主隊列,最有可能是更新UI的時候。 接下來的代碼塊將展示在其他隊列中運行主隊列,根據你的需要,你可以指定同步或異步來執行你的代碼:
Dispatchqueue.main.async {
// Do something
}
在Xcode里,當你輸入`Dispatchqueue.main.`(記住最后有個.),Xcode將會自動提示所有你能在主隊列中訪問的方法(實際上這個技巧適應于所有隊列,操作方法是在隊列名稱后帶`.`),上述代碼就是主線程你所需要知道的內容,你也可以像自己創建隊列那樣,添加延遲執行代碼塊。
接下來將實操下主線程怎么更新UI,初始項目的`Main.storyboard`中有個`ViewController`場景包含了一個圖片視圖,通過`IBOutlet`屬性連接到了`ViewController`類中,在`ViewController`類中找到`fetchImage()`方法, 補充上下載Appcoda logo和在圖片視圖中展示它的代碼,如下所示(這里不討論`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()`方法,編譯運行下程序結果如下(記得調用`fetchImage()`方法):
雖然我們已經下載了圖片并打印了相關信息,因為ui沒有更新導致圖片也沒有顯示出來,最有可能發生的情況是圖片在打印信息的一段時間后顯示(如果還有其他任務在運行,可能會要等更久),但是問題不止這一個,控制臺輸出了一大串關于在后臺線程中更新ui的警告日志。
現在糾正到主線程來更新UI,將相關代碼更新如下:
if let data = imageData {
print("Did download image data")
Dispatchqueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
重新運行下app,因為主隊列被喚起并更新了UI,圖片在下載后立即就顯示出來了。
使用DispatchWorkItem對象
DispatchWorkItem 是一個代碼塊,它可以被分到任何的隊列,包含的代碼可以在后臺或主線程中被執行,簡單來說:它被用于替換我們前面寫的代碼block來調用
最簡單的使用WorkItem的例子如下:
let workItem = DispatchWorkItem {
// Do something
}
接下來展示一個小示例來闡述`DispatchWorkItem`的使用方法,找到`useWorkItem()`方法,在其中添加如下代碼:
func useWorkItem() {
var value = 10
let workItem = DispatchWorkItem {
value += 5
}
}
workItem用于讓value每次遞增5,如下代碼所示,通過調用`workItem`的`perform()`方法來激活
workItem.perform()
默認將會在主線程執行,但你可以選擇在其他隊列中執行,比如:
let queue = Dispatchqueue.global()
queue.async {
workItem.perform()
}
這樣也一樣會運行正常。為了更簡潔的實現同樣目的,`DispatchQueue`提供了另外一個方法:
queue.async(execute: workItem)
當一個WorkItem被分發,你可以通知主隊列(或其他隊列)來做一些后續的處理:
workItem.notify(queue: Dispatchqueue.main) {
print("value = ",value)
}
以上代碼將會在控制臺打印`value`對象的值,它將在workItem被分發后被調用,整理下代碼,使用workItem的完整用法如下:
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)
}
}
以下是運行結果(`useWorkItem()`已經在`viewDidAppear(_:)`中被調用了:
總結
這篇博文包含的內容應該滿足大部分你將要使用到多任務和并發編程的場景,但也沒有那么全面包含了GCD的方方面面,可能博文中有引到但是沒有具體說明的內容,總體來說,我希望博文能夠盡量的簡單,從而被廣大開發者所理解和接受。 如果你平常都沒有用到GCD,那你應該考慮使用GCD來為你的`main queue`減負, 主動承擔一些比較重的操作,能在后臺運行的任務絕不在`main queue`上運行。 淺顯易懂的GCD能夠非常有效的加快應用的運行和響應速度, 你值得擁有。
譯者雜耍時間:
Q1: 為啥代碼中藍點沒有比紅點先執行?
A1: 為了簡潔說明, 去掉了for循環, 答案如下:
懵逼了, 好好的全局后端隊列,怎么跑到主線程上去執行了. 答案就是蘋果為了優化性能, sync會盡可能在當前線程來運行
Q2: 探討下崩潰問題:
好冤枉啊, 我不就想明明白白的在主線程上同步的運行么, 為啥說崩就崩了?
A2: 答案是死鎖了
主線程是串行的, 上個任務執行完成才會繼續下個任務, `simpleQueues()`整個方法相當于mainQueue的一個任務(任務A), 現在它里面加了個sync的{任務A1}, 意味著 任務A1 只有等任務A完成才能開始, 但是要完成任務A的話就必須先完成任務A1, 然后A又在等A1,然后就傻逼了, 邏輯好繞吧????....
來自:http://www.cocoachina.com/swift/20170223/18749.html