本文由loveltyoic(博客)翻譯自raywenderlich,原文:Grand Central Dispatch Tutorial for Swift: Part 1/2
歡迎來(lái)到本GCD教程的第二同時(shí)也是最終部分!
在第一部分中,你學(xué)到了并發(fā),線程以及GCD的工作原理。通過(guò)使用dispatch_barrrier和dispatch_sync,你做到了讓PhotoManager單例在讀寫(xiě)照片時(shí)是線程安全的。除此之外,你用到dispatch_after來(lái)提示用戶,優(yōu)化了用戶體驗(yàn)。還有,使用dispatch_async異步執(zhí)行CPU密集型任務(wù),從而為視圖控制器初始化過(guò)程減負(fù)。
如果你跟著教程做,現(xiàn)在可以從第一部分的示例工程繼續(xù)。如果你沒(méi)有完成第一部分或不想再用你的工程,可以下載第一部分的完成文件。
是時(shí)候進(jìn)一步探索GCD了!
糾正過(guò)早出現(xiàn)的彈窗
你可能注意到,當(dāng)你通過(guò) Le Internet 選項(xiàng)添加照片時(shí),會(huì)有提示框在圖片下載完成之前就彈出,如下圖:
錯(cuò)誤在于 PhotoManager 里的 downloadPhotosWithCompletion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) { var storedError: NSError! for address in [OverlyAttachedGirlfriendURLString, SuccessKidURLString, LotsOfFacesURLString] { let url = NSURL(string: address) let photo = DownloadPhoto(url: url!) { image, error in if error != nil { storedError = error } } PhotoManager.sharedManager.addPhoto(photo) } if let completion = completion { completion(error: storedError) } } |
這里在方法的最后調(diào)用completion閉包——你會(huì)想當(dāng)然的認(rèn)為所有圖片都下載完了。但不幸的是,在此時(shí)無(wú)法保證。
DownloadPhoto類的實(shí)例方法從一個(gè)URL下載圖片并且不等下載完成就立即退出。換言之,downloadPhotosWithCompletion在最后調(diào)用completion閉包,就好像其中的所有方法都在順序執(zhí)行,并且在每個(gè)方法完成后才執(zhí)行下一個(gè)。
然而,DownloadPhoto(url:)是異步并且立即返回的——所以目前的方式不能正常工作。
downloadPhotosWithCompletion應(yīng)該在所有圖片下載任務(wù)都完成后再調(diào)用自己的completion閉包。問(wèn)題是:你怎么監(jiān)視并發(fā)的異步事件呢?你不知道它們何時(shí)完成,以何種順序。
也許你可以用多個(gè)Bool值來(lái)追蹤下載情況,但那不容易擴(kuò)展。而且坦白講,那是很丑陋的代碼。
幸運(yùn)的是,dispatch groups就是專為監(jiān)視多個(gè)異步任務(wù)的完成情況而設(shè)計(jì)的。
調(diào)度組(Dispatch Groups)
調(diào)度組在一組任務(wù)都完成后會(huì)發(fā)出通知。這些任務(wù)可以是異步或同步的,甚至可以分布在不同的隊(duì)列。調(diào)度組還可以通過(guò)同步或異步的方式來(lái)通知。因?yàn)槿蝿?wù)在不同的隊(duì)列中,disptch_group_t實(shí)例用來(lái)追蹤隊(duì)列中的不同任務(wù)。
在組內(nèi)所有事件都完成時(shí),GCD API提供了兩種方式發(fā)送通知。
第一種是dispatch_group_wait,它會(huì)阻塞當(dāng)前進(jìn)程,直到所有任務(wù)都完成或是等待超時(shí)。這正是我們的例子中需要的方式。
打開(kāi) PhotoManager.swift ,替換downloadPhotosWithCompletion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) { dispatch_async(GlobalUserInitiatedQueue) { // 1 var storedError: NSError! var downloadGroup = dispatch_group_create() // 2 for address in [OverlyAttachedGirlfriendURLString, SuccessKidURLString, LotsOfFacesURLString] { let url = NSURL(string: address) dispatch_group_enter(downloadGroup) // 3 let photo = DownloadPhoto(url: url!) { image, error in if let error = error { storedError = error } dispatch_group_leave(downloadGroup) // 4 } PhotoManager.sharedManager.addPhoto(photo) } dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER) // 5 dispatch_async(GlobalMainQueue) { // 6 if let completion = completion { // 7 completion(error: storedError) } } } } |
逐一來(lái)看注釋:
因?yàn)槭褂胐ispatch_group_wait阻塞了當(dāng)前進(jìn)程,要用dispatch_async將整個(gè)方法放到后臺(tái)隊(duì)列,才能保證主線程不被阻塞。
創(chuàng)建一個(gè)調(diào)度組,作用好比未完成任務(wù)的計(jì)數(shù)器。
dispatch_group_enter通知調(diào)度組一個(gè)任務(wù)已經(jīng)開(kāi)始。你必須保證dispatch_group_enter和dispatch_group_leave是成對(duì)調(diào)用的,否則程序會(huì)崩潰。
通知任務(wù)已經(jīng)完成。再一次,這里保持進(jìn)和出相匹配。
dispatch_group_wait等待所有任務(wù)都完成直到超時(shí)。如果在任務(wù)完成前就超時(shí)了,函數(shù)會(huì)返回一個(gè)非零值。可以通過(guò)返回值來(lái)判斷是否等待超時(shí);不過(guò),這里你用DISPATCH_TIME_FOREVER來(lái)表示一直等待。這意味著,它會(huì)永遠(yuǎn)等待!沒(méi)關(guān)系,因?yàn)閳D片總是會(huì)下載完的。
此時(shí),你可以保證所有圖片任務(wù)都完成或是超時(shí)了。接下來(lái)在主隊(duì)列中加入完成閉包。閉包晚些時(shí)候會(huì)在主線程中執(zhí)行。
執(zhí)行閉包。
運(yùn)行app,下載幾張圖片,留意你的app是如何表現(xiàn)的。
Note:如果網(wǎng)速太快以至于分辨不出何時(shí)執(zhí)行的閉包,你可以修改設(shè)備的設(shè)置。在 Setting 中的Developer Section 。打開(kāi) Network Link Conditioner,選擇“Very Bad Network”。
如果在模擬器上,用工具變更網(wǎng)速。這是你武器庫(kù)中一個(gè)很好的工具,它讓你清楚在不佳的網(wǎng)絡(luò)下你的app會(huì)發(fā)生什么。
這個(gè)方案目前不錯(cuò),但最好能避免阻塞進(jìn)程。你下一步的工作是重寫(xiě)這個(gè)方法來(lái)異步通知下載完成。
在學(xué)習(xí)下一個(gè)調(diào)度組的用法前,先看看怎樣在不同的隊(duì)列類型下使用調(diào)度組。
自定義順序隊(duì)列:好選擇。當(dāng)一組任務(wù)完成時(shí)用它發(fā)送通知。
主隊(duì)列(順序):在當(dāng)前情景下是不錯(cuò)的選擇。但你要謹(jǐn)慎地在主隊(duì)列中使用,因?yàn)橥降却腥蝿?wù)會(huì)阻塞主線程。然而,當(dāng)一個(gè)需要較長(zhǎng)時(shí)間的任務(wù)(比如網(wǎng)絡(luò)請(qǐng)求)完成時(shí),異步更新UI是很好的選擇。
并發(fā)隊(duì)列:好選擇。用于調(diào)度組和通知。
調(diào)度組,再來(lái)一次
做的不錯(cuò),但是異步調(diào)度到另一個(gè)隊(duì)列然后用 dispatch_group_wait 阻塞還是有一些笨拙。還有另一種方式…
在 PhotoManager.swift 中找到downloadPhotosWithCompletion并替換之:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) { // 1 var storedError: NSError! var downloadGroup = dispatch_group_create() for address in [OverlyAttachedGirlfriendURLString, SuccessKidURLString, LotsOfFacesURLString] { let url = NSURL(string: address) dispatch_group_enter(downloadGroup) let photo = DownloadPhoto(url: url!) { image, error in if let error = error { storedError = error } dispatch_group_leave(downloadGroup) } PhotoManager.sharedManager.addPhoto(photo) } dispatch_group_notify(downloadGroup, GlobalMainQueue) { // 2 if let completion = completion { completion(error: storedError) } } } |
異步方法是如何工作的:
新的實(shí)現(xiàn)不需要把方法放進(jìn)dispatch_async中,因?yàn)槟悴](méi)有阻塞主線程。
dispatch_group_notify異步執(zhí)行閉包。當(dāng)調(diào)度組內(nèi)沒(méi)有剩余任務(wù)的時(shí)候閉包才執(zhí)行。同樣要指明在哪個(gè)隊(duì)列中執(zhí)行閉包。當(dāng)下,你需要在主隊(duì)列中執(zhí)行閉包。
這是更優(yōu)雅的方法,并且不會(huì)阻塞任何進(jìn)程。
并發(fā)過(guò)多帶來(lái)的危險(xiǎn)
通過(guò)支配這些新工具,你應(yīng)該將每件事都線程化,對(duì)嗎?
看看PhotoManager中的downloadPhotosWithCompletion。你會(huì)發(fā)現(xiàn)通過(guò)for循環(huán)下載了三張圖片。現(xiàn)在來(lái)看看能否通過(guò)并發(fā)執(zhí)行for循環(huán)來(lái)提速。
是時(shí)候請(qǐng)出dispatch_apply了。
dispatch_apply像for循環(huán)一樣,只不過(guò)它會(huì)并發(fā)地執(zhí)行循環(huán)過(guò)程。這個(gè)函數(shù)是同步的,所以像普通的for循環(huán)一樣,dispatch_apply在所有工作都完成后才返回。
要注意循環(huán)的最佳次數(shù),如果有太多循環(huán)但每個(gè)循環(huán)內(nèi)只有很小的工作量,那么額外的開(kāi)銷(xiāo)會(huì)抹殺掉并發(fā)帶來(lái)的好處。 步進(jìn) (striding)可以幫助到你。它讓你在每次循環(huán)中做多件工作。
什么時(shí)候用dispatch_apply合適?
自定義順序隊(duì)列:在順序隊(duì)列中使用dispatch_apply完全無(wú)意義;它的效果和for循環(huán)一樣。
主隊(duì)列(順序):理由同上,用for循環(huán)就可以了。
并發(fā)隊(duì)列:明智之選,尤其是你需要追蹤任務(wù)進(jìn)度時(shí)。
替換downloadPhotosWithCompletion如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) { var storedError: NSError! var downloadGroup = dispatch_group_create() let addresses = [OverlyAttachedGirlfriendURLString, SuccessKidURLString, LotsOfFacesURLString] dispatch_apply(UInt(addresses.count), GlobalUserInitiatedQueue) { i in let index = Int(i) let address = addresses[index] let url = NSURL(string: address) dispatch_group_enter(downloadGroup) let photo = DownloadPhoto(url: url!) { image, error in if let error = error { storedError = error } dispatch_group_leave(downloadGroup) } PhotoManager.sharedManager.addPhoto(photo) } dispatch_group_notify(downloadGroup, GlobalMainQueue) { if let completion = completion { completion(error: storedError) } } } |
現(xiàn)在你的循環(huán)可以并發(fā)執(zhí)行了;調(diào)用 dispatch_apply 時(shí),第一個(gè)參數(shù)是循環(huán)的次數(shù),第二個(gè)參數(shù)是執(zhí)行任務(wù)的隊(duì)列,第三個(gè)參數(shù)是閉包。
盡管你的代碼在添加圖片時(shí)是線程安全的,但是圖片的順序取決于線程完成的順序。
運(yùn)行app,用 Le Internet 添加一些圖片,發(fā)現(xiàn)不同了嗎?
在真機(jī)上運(yùn)行新的代碼會(huì)發(fā)現(xiàn) 些許 的速度提升。但是這值得嗎?
實(shí)際上,在這里并不值得這么做。原因如下:
你很可能因?yàn)椴⑿卸ㄙM(fèi)了比f(wàn)or循環(huán)更多的開(kāi)銷(xiāo)。你應(yīng)該結(jié)合合適的步長(zhǎng)對(duì) 非常大 的集合使用dispatch_apply。
開(kāi)發(fā)app的時(shí)間有限——不要花時(shí)間過(guò)早優(yōu)化。如果你想優(yōu)化,那么就優(yōu)化那些值得優(yōu)化的東西。用Instruments測(cè)試app以找到最耗時(shí)間的方法。如何使用Instruments。
一般說(shuō)來(lái),代碼優(yōu)化會(huì)讓你的代碼變得更復(fù)雜。你要確定帶來(lái)的好處值得你增加復(fù)雜性。
記住,不要癡迷于優(yōu)化。否則只會(huì)讓你自己為難,也讓看你代碼的人抓狂。
取消調(diào)度塊
iOS 8 和 OS X Yosemite引入了 調(diào)度對(duì)象塊 (dispatch block object)。它們實(shí)現(xiàn)起來(lái)就像對(duì)閉包再包裝一層。調(diào)度對(duì)象塊可以做到很多事情,比如為隊(duì)列中的對(duì)象設(shè)置QoS等級(jí)來(lái)決定優(yōu)先級(jí),但最顯著的能力是可以取消塊的執(zhí)行。要明白對(duì)象塊只有在輪到它執(zhí)行之前才可以取消(一旦開(kāi)始執(zhí)行就不能取消了)。
為了說(shuō)明這個(gè)問(wèn)題,首先用 Le Internet 下載一些圖片,然后取消它們。替換 PhotoManager.swift 中的downloadPhotosWithCompletion:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) { var storedError: NSError! let downloadGroup = dispatch_group_create() var addresses = [OverlyAttachedGirlfriendURLString, SuccessKidURLString, LotsOfFacesURLString] addresses += addresses + addresses // 1 var blocks: [dispatch_block_t] = [] // 2 for i in 0 ..< addresses.count { dispatch_group_enter(downloadGroup) let block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // 3 let index = Int(i) let address = addresses[index] let url = NSURL(string: address) let photo = DownloadPhoto(url: url!) { image, error in if let error = error { storedError = error } dispatch_group_leave(downloadGroup) } PhotoManager.sharedManager.addPhoto(photo) } blocks.append(block) dispatch_async(GlobalMainQueue, block) // 4 } for block in blocks[3 ..< blocks.count] { // 5 let cancel = arc4random_uniform(2) // 6 if cancel == 1 { dispatch_block_cancel(block) // 7 dispatch_group_leave(downloadGroup) // 8 } } dispatch_group_notify(downloadGroup, GlobalMainQueue) { if let completion = completion { completion(error: storedError) } } } |
擴(kuò)展addresses數(shù)組,將每個(gè)地址復(fù)制3份。
這個(gè)數(shù)組用來(lái)保存接下來(lái)創(chuàng)建的對(duì)象塊。
dispatch_block_create創(chuàng)建一個(gè)對(duì)象塊。第一個(gè)參數(shù)是一個(gè)表明了塊特征的標(biāo)志。此處的標(biāo)志讓塊從它進(jìn)入的隊(duì)列那里繼承QoS等級(jí)。第二個(gè)參數(shù)是閉包形式的塊定義。
塊被異步的調(diào)度到全局主隊(duì)列。這里用全局主隊(duì)列是因?yàn)樗且粋€(gè)順序隊(duì)列,可以方便我們?nèi)∠麑?duì)象塊。當(dāng)前代碼已經(jīng)在主線程中執(zhí)行著,所以你可以保證下載任務(wù)將在此之后才執(zhí)行(也就是這個(gè)downloadPhotosWithCompletion返回后才輪到下載任務(wù)執(zhí)行)。
取數(shù)組中第三個(gè)到結(jié)尾的部分。
arc4random_uniform會(huì)隨機(jī)返回一個(gè)0到上界之間(不含上界)的整數(shù)。以2為上界會(huì)得到0或1,像投硬幣一樣。
如果隨機(jī)數(shù)是1,則取消塊。前提是,塊還在隊(duì)列中并且沒(méi)開(kāi)始。塊在執(zhí)行的過(guò)程中是不可以取消的。
因?yàn)樗袎K都加入調(diào)度組了,不要忘記移除被取消的那些塊。
運(yùn)行,從 Le Internet 添加圖片。你會(huì)看到app下載3張圖片,以及隨機(jī)數(shù)量的額外圖片。那些沒(méi)下載的圖片是因?yàn)樵诩尤腙?duì)列 后 被取消了。這是一個(gè)刻意設(shè)計(jì)的例子,但是很好的演示了怎樣使用調(diào)度對(duì)象塊以及如何取消它。
調(diào)度對(duì)象塊能做更多事情,別忘了查看文檔。
五花八門(mén)的GCD趣用
等等!還有更多!下面展示一些常規(guī)用途之外的功能。盡管你不會(huì)經(jīng)常使用這些工具,但他們可能在特定情況下非常有用。
測(cè)試異步代碼
這聽(tīng)起來(lái)很瘋狂,但是你知道Xcode擁有測(cè)試功能嗎?:]我知道,有時(shí)我喜歡假裝它不存在,但是編寫(xiě)和運(yùn)行測(cè)試對(duì)構(gòu)建復(fù)雜的代碼很重要。
Xcode中的測(cè)試運(yùn)行在XCTestCase的子類之下,它會(huì)運(yùn)行所有以test開(kāi)頭的方法。測(cè)試跑在主線程下,所以你可以認(rèn)為測(cè)試是順序執(zhí)行的。
一旦給定的測(cè)試方法返回了,XCTest 會(huì)認(rèn)為這個(gè)測(cè)試完成了而去做下一個(gè)測(cè)試。這就是說(shuō),在下一個(gè)測(cè)試執(zhí)行過(guò)程中,前一個(gè)測(cè)試中的異步代碼也在繼續(xù)執(zhí)行。
網(wǎng)路請(qǐng)求通常是異步的,因?yàn)槟悴幌胱枞骶€程。一旦測(cè)試方法返回,測(cè)試也就結(jié)束了,因此很難對(duì)網(wǎng)絡(luò)請(qǐng)求做測(cè)試。
我們簡(jiǎn)單看一下兩種普遍的測(cè)試異步代碼的方法:信號(hào)量(semaphores)和 期望(expectations)。
信號(hào)量
信號(hào)量是一個(gè)古老學(xué)院派的線程概念,它是由謙遜的Edsger W. Dijkstra提出的。信號(hào)量是很復(fù)雜的話題,因?yàn)樗⒃阱e(cuò)綜復(fù)雜的操作系統(tǒng)函數(shù)之上。
如果你想了解更多信號(hào)量的知識(shí),查閱細(xì)說(shuō)信號(hào)量原理。如果你是學(xué)院派,有一個(gè)用到了信號(hào)量的經(jīng)典軟件開(kāi)發(fā)問(wèn)題叫做哲學(xué)家進(jìn)餐問(wèn)題。
信號(hào)量讓你控制多個(gè)消費(fèi)者對(duì)有限資源的獲取。例如,如果你創(chuàng)建一個(gè)信號(hào)量來(lái)控制擁有2個(gè)資源的資源池,那么同一時(shí)刻最多有兩個(gè)線程可以進(jìn)入臨界區(qū)。其它也想使用資源的線程必須在FIFO隊(duì)列中等待。
打開(kāi) GooglyPuffTests.swift 并替換掉 downloadImageURLWithString:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | func downloadImageURLWithString(urlString: String) { let url = NSURL(string: urlString) let semaphore = dispatch_semaphore_create(0) // 1 let photo = DownloadPhoto(url: url!) { image, error in if let error = error { XCTFail( "/(urlString) failed. /(error.localizedDescription)" ) } dispatch_semaphore_signal(semaphore) // 2 } let timeout = dispatch_time(DISPATCH_TIME_NOW, DefaultTimeoutLengthInNanoSeconds) if dispatch_semaphore_wait(semaphore, timeout) != 0 { // 3 XCTFail( "/(urlString) timed out" ) } } |
以上代碼中信號(hào)量的工作原理:
1. 創(chuàng)建信號(hào)量。參數(shù)表明信號(hào)量起始值。這個(gè)值代表了起始階段可以獲取信號(hào)量的線程數(shù)目(增加信號(hào)量就是發(fā)信號(hào),用0做初始值代表當(dāng)前沒(méi)有線程可以獲取信號(hào)量)。 2. 在完成閉包中,你告訴信號(hào)量不再需要資源。這會(huì)使信號(hào)量增加,同時(shí)給其他等待資源的任務(wù)發(fā)信號(hào),通知當(dāng)前信號(hào)量可用。
3. 等待信號(hào)量并設(shè)置超時(shí)時(shí)間。這個(gè)調(diào)用會(huì)阻塞當(dāng)前進(jìn)程直到收到信號(hào)。非0返回表示等待已超時(shí)。在這種情況下,測(cè)試失敗,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求不應(yīng)該超過(guò)10秒——相當(dāng)合理的假設(shè)!
(譯者注:說(shuō)下我的理解:首先創(chuàng)建了信號(hào)量,但此時(shí)因?yàn)樾盘?hào)量是0,沒(méi)有線程可以獲取它,注釋3中對(duì)信號(hào)量的等待會(huì)阻塞。只有在圖片下載好了以后,才會(huì)發(fā)送一個(gè)信號(hào)量,那么注釋3對(duì)信號(hào)量的獲取就成功了,并退出等待。但如果圖片下載失敗呢?就不會(huì)調(diào)用注釋2這句觸發(fā)信號(hào)的語(yǔ)句,那么注釋3就會(huì)等待超時(shí),從而測(cè)試失敗。)
PRoduct/Test 或 cmd+U 運(yùn)行測(cè)試。測(cè)試應(yīng)該成功。
斷掉網(wǎng)絡(luò)連接并再次測(cè)試;如果在真機(jī)測(cè)試,請(qǐng)開(kāi)啟飛行模式。如果在模擬器上,直接斷網(wǎng)就好了。測(cè)試在10秒后會(huì)返回失敗的結(jié)果。很好,起作用了!
這是相當(dāng)微不足道的測(cè)試,但是如果你和服務(wù)端團(tuán)隊(duì)一起工作,這些基礎(chǔ)測(cè)試可以避免一些涉及網(wǎng)絡(luò)問(wèn)題的無(wú)端指責(zé)。
期望(expectations)
XCTest框架提供了另一種使用 期望 來(lái)測(cè)試異步代碼的方法。這種特性讓你首先設(shè)置你的期望——你希望發(fā)生的事——然后再開(kāi)始異步任務(wù)。接下來(lái)測(cè)試會(huì)一直等待,直到異步任務(wù)將期望標(biāo)記為 已完成 。
替換 GooglyPuffTests.swift 中的downloadImageURLWithString:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | func downloadImageURLWithString(urlString: String) { let url = NSURL(string: urlString) let downloadExpectation = expectationWithDescription( "Image downloaded from /(urlString)" ) // 1 let photo = DownloadPhoto(url: url!) { image, error in if let error = error { XCTFail( "/(urlString) failed. /(error.localizedDescription)" ) } downloadExpectation.fulfill() // 2 } waitForExpectationsWithTimeout(10) { // 3 error in if let error = error { XCTFail(error.localizedDescription) } } } |
工作原理:
1. 用expectationWithDescription生成期望。測(cè)試會(huì)在日志上顯示其中的字符串參數(shù),所以請(qǐng)描述你期望發(fā)生的事。
2. 在異步執(zhí)行的閉包中調(diào)用fulfill來(lái)標(biāo)記期望已達(dá)成。
3. 調(diào)用線程用waitForExpectationsWithTimeout等待期望達(dá)成。如果等待超時(shí)會(huì)視為出錯(cuò)。
運(yùn)行測(cè)試。結(jié)果和使用信號(hào)量沒(méi)什么不同,但使用XCTest框架是更清晰易讀的方案。
調(diào)度源(Dispatch Sources)
GCD中存在一個(gè)特別有趣的特性叫調(diào)度源,它是一個(gè)包含底層功能的百寶囊,幫助你響應(yīng)或監(jiān)控Unix信號(hào),文件描述符(file descriptors),Mach端口,VFS Nodes,以及其他復(fù)雜的東西。所有這些都超出了本教程的范圍,但是你可以嘗試著使用一下調(diào)度源對(duì)象。
第一次使用調(diào)度源的用戶可能會(huì)迷失其中,所以你首先要理解dispatch_source_create的工作原理。下面是創(chuàng)建它的函數(shù)原型:
1 2 3 4 5 | func dispatch_source_create( type: dispatch_source_type_t, handle: UInt, mask: UInt, queue: dispatch_queue_t!) -> dispatch_source_t! |
第一個(gè)參數(shù)type: dispatch_source_type_t是最重要的參數(shù),因?yàn)樗枋隽司浔╤andle)和掩碼(mask)參數(shù)。你需要查看Xcode文檔來(lái)弄清楚dispatch_source_type_t的參數(shù)有哪些可選項(xiàng)。
這里你會(huì)監(jiān)視DISPATCH_SOURCE_TYPE_SIGNAL。如文檔所述:
調(diào)度源監(jiān)控當(dāng)前進(jìn)程的信號(hào)。句柄(handle)是信號(hào)數(shù)字(int)。掩碼(mask)沒(méi)用到(傳0)。
Unix信號(hào)列表可以從signal.h找到。在頂部有一串#define。在這些信號(hào)列表中,你將要監(jiān)控SIGSTOP信號(hào)。這個(gè)信號(hào)會(huì)在進(jìn)程接收到不可抗拒的掛起指令時(shí)被發(fā)送。這個(gè)信號(hào)與你用LLDB debugger調(diào)試程序時(shí)發(fā)送的信號(hào)相同。
進(jìn)入 PhotoCollectionViewController.swift ,在viewDidLoad附近添加下面的代碼。你需要為類添加兩個(gè)私有屬性,并在viewDidLoad的開(kāi)始處添加段代碼,在調(diào)用superclass和ALAssetLibrary之間:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #if DEBUG private var signalSource: dispatch_source_t! private var signalOnceToken = dispatch_once_t() #endif override func viewDidLoad() { super .viewDidLoad() #if DEBUG // 1 dispatch_once(&signalOnceToken) { // 2 let queue = dispatch_get_main_queue() self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, UInt(SIGSTOP), 0, queue) // 3 if let source = self.signalSource { // 4 dispatch_source_set_event_handler(source) { // 5 NSLog( "Hi, I am: /(self.description)" ) } dispatch_resume(source) // 6 } } #endif // The other stuff } |
這段代碼有點(diǎn)難懂,因此逐個(gè)注釋來(lái)講解:
1. 最好只在DEBUG模式下編譯這段代碼,因?yàn)檫@可能讓不懷好意者洞見(jiàn)很多信息。:] 在 Project Settings –> Build Settings –> Swift Compiler – Custom Flags –> Other Swift Flags –> Debug 下添加 -D DEBUG 。
2. 用dispatch_once一次性初始化調(diào)度源。
3. 初始化signalSource變量。你指明對(duì)信號(hào)感興趣并且提供SIGSTOP做第二個(gè)參數(shù)。除此之外,你用主隊(duì)列處理接收到的事件——稍后你會(huì)發(fā)現(xiàn)為什么。
4. 如果參數(shù)錯(cuò)誤,調(diào)度源對(duì)象不會(huì)被創(chuàng)建。因此,你應(yīng)該在使用它之前確保調(diào)度源是有效的。
5. dispatch_source_set_event_handler注冊(cè)了一個(gè)事件處理閉包,當(dāng)你接收到監(jiān)控的信號(hào)時(shí)會(huì)調(diào)用這個(gè)閉包。
6. 默認(rèn)情況下,所有調(diào)度源在開(kāi)始都處于掛起狀態(tài)。當(dāng)你想監(jiān)視事件時(shí),必須讓源對(duì)象繼續(xù)執(zhí)行。
運(yùn)行app;暫停調(diào)試器然后立即恢復(fù)。檢查控制臺(tái)(console),你會(huì)看到類似下面的信息:
1 | 2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am: |
你的app現(xiàn)在可以感知到調(diào)試(debugging-aware)了!這真棒,但在現(xiàn)實(shí)中怎樣用它呢?
你可以用它調(diào)試一個(gè)對(duì)象并在恢復(fù)app時(shí)展示數(shù)據(jù);你也可以自定義一些安全邏輯來(lái)保護(hù)app,當(dāng)惡意攻擊者在你的程序上附著調(diào)試器的時(shí)候。
有趣的想法是把這個(gè)方法當(dāng)做堆棧追蹤工具,來(lái)找到你想要在調(diào)試器中修改的對(duì)象。
設(shè)想一下這樣的場(chǎng)景。當(dāng)你意外地停掉調(diào)試器時(shí),你很難處在期望的棧幀上。而現(xiàn)在你可以在任意時(shí)刻停止調(diào)試器并讓代碼執(zhí)行到你期望的位置。這很有用,當(dāng)你想執(zhí)行一段從調(diào)試器很難達(dá)到的代碼。試一試!
在viewDidLoad中的NSLog語(yǔ)句處設(shè)置斷點(diǎn)。暫停調(diào)試器,然后再開(kāi)始;app會(huì)命中你剛剛設(shè)置的斷點(diǎn)。現(xiàn)在你已經(jīng)深入到PhotoCollectionViewController方法中了。現(xiàn)在你可以隨心所欲地使用PhotoCollectionViewController實(shí)例了。多么便捷!
注意:如果在調(diào)試器中你不知道哪個(gè)線程是哪個(gè),來(lái)看一下。主線程總是第一個(gè),libdispatch,GCD的協(xié)調(diào)器是第二個(gè)。剩下的線程要看硬件當(dāng)時(shí)在做什么樣的工作。
在調(diào)試器中,輸入:
1 | po self.navigationItem.prompt = "WOOT!" |
然后繼續(xù)執(zhí)行app。你會(huì)看到如下所示:
通過(guò)這個(gè)方法,你可以更新UI,探查類的屬性,甚至執(zhí)行方法——無(wú)需重啟app來(lái)進(jìn)入特定的工作流狀態(tài)。很巧妙。
下一步?
我不想重提,但是你真的應(yīng)該看一下怎樣使用Instruments。如果你想優(yōu)化app,絕對(duì)需要這個(gè)。Instruments可以概述程序中哪些代碼相對(duì)其它代碼執(zhí)行更久。如果你想知道代碼實(shí)際的執(zhí)行時(shí)間,很可能需要一些自制的解決方案。
同時(shí)學(xué)習(xí)如何在Swift中使用NSOperations和NSOperationQueue,一種基于GCD的并發(fā)技術(shù)。實(shí)際上,這是使用GCD的最佳實(shí)踐。NSOperations提供更好的控制,處理最多的并發(fā)操作,在犧牲一定速度的情況下更加面向?qū)ο蟆?/span>
記住,除非你有特別的理由深入底層,你應(yīng)該始終嘗試并堅(jiān)持使用更高層的API。只在你想學(xué)習(xí)更多或做一些非常非常“有趣”的事時(shí)才進(jìn)入到Apple的“暗黑藝術(shù)”(dark art)中探險(xiǎn)。:]
祝你好運(yùn),盡情歡樂(lè)!
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注