Grand Central Dispatch 教程
在教程的***部分,你學(xué)到了一些關(guān)于并發(fā),線程及GCD工作原理的知識(shí)。你通過(guò)并用dispatch_barrier_async與dispatch_sync保證了PhotoManager單例在讀取與寫入照片過(guò)程中的線性安全性。值得一提的是,你不僅通過(guò)dispatch_after及時(shí)地向用戶發(fā)出提醒以此優(yōu)化了App的UX而且還通過(guò)dispatch_async將部分工作從一個(gè)View Controller的實(shí)例化過(guò)程中分割至另一線程以此實(shí)現(xiàn)CPU高密度處理工作。
假如你是一路從上一部分教程學(xué)過(guò)來(lái)的話,你完全可以在以前的工程文件上繼續(xù)Coding。但假如你沒(méi)有完成教程的***部分或是不想繼續(xù)使用自己的工程文件的話,你可從這里下載到教程***部分的完整工程文件。
OK! 是時(shí)候探索一下更多關(guān)于GCD的知識(shí)了。
修復(fù)提早出現(xiàn)的Popup
也許你已經(jīng)注意到了當(dāng)你通過(guò)Le Internet的方式添加照片時(shí),在所有照片下載完成前AlertView就已經(jīng)跳出來(lái)提醒你“Download Complete”。
See That?
其實(shí)問(wèn)題出在PhotoManaer的downloadPhotosWithCompletion函數(shù)中:
- 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)
- }
- }
在函數(shù)結(jié)尾部分調(diào)用completion閉包--這就代表著你認(rèn)為所有的照片的下載任務(wù)都已經(jīng)完成。但不幸的是此時(shí)此刻你并無(wú)法保證所有的下載任務(wù)都已經(jīng)完成。
DownloadPhoto類的實(shí)例化方法開(kāi)始從一個(gè)URL下載文件并在下載完成前立即返回值。換句話說(shuō),downloadPhotosWithCompletion在函數(shù)結(jié)尾處調(diào)用其自己的completion閉包就好像它自己就是個(gè)有著直線型同步執(zhí)行代碼的方法體,并且每個(gè)方法執(zhí)行完自己的工作后都會(huì)調(diào)用這個(gè)completed。
不管怎樣,DownloadPhoto(url:)是異步執(zhí)行的并且立即返回--所以這個(gè)解決方案不管用。
再有,downloadPhotosWithCompletion應(yīng)該在所有的照片下載任務(wù)都調(diào)用了completion閉包后再調(diào)用自己的completion閉包。那么問(wèn)題來(lái)了:你怎樣去監(jiān)管那些同時(shí)執(zhí)行的異步事件呢?你根本不會(huì)知道它們會(huì)何時(shí)并以何種順序結(jié)束。
或許你可以寫多個(gè)Bool值去跟蹤每個(gè)任務(wù)的下載狀態(tài)。說(shuō)實(shí)話,這樣做的話會(huì)感覺(jué)有些low并且代碼看起來(lái)會(huì)很亂。
萬(wàn)幸的是,派發(fā)組(dispatch groups)正是為滿足監(jiān)管這種多異步completion的需要所設(shè)計(jì)的。
派發(fā)組(Dispatch Group)
當(dāng)整組的任務(wù)都完成時(shí)派發(fā)組會(huì)提醒你。這些任務(wù)既可以是異步的也可以是同步的并且可以在不同隊(duì)列中被監(jiān)管。當(dāng)全組任務(wù)完成時(shí)派發(fā)組可以通過(guò)同步或異步的方式提醒你。只要有任務(wù)在不同隊(duì)列中被監(jiān)管,dispatch_group_t實(shí)例便會(huì)在多個(gè)隊(duì)列中的持續(xù)監(jiān)管這些不同的任務(wù)。
當(dāng)組中的全部任務(wù)執(zhí)行完畢后,GCD的API提供兩種方式向你發(fā)出提醒。
***個(gè)便是dispatch_group_wait,這是一個(gè)在組內(nèi)所有任務(wù)執(zhí)行完畢前或在處理超時(shí)的情況下限制當(dāng)前線程運(yùn)行的函數(shù)。在AlertView提早出現(xiàn)的情況下,使用disptach_group_wait絕對(duì)是你的***解決方案。
打開(kāi)PhotoManager.swift并用如下代碼替換原downloadPhotosWithCompletion函數(shù):
- 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)
- }
- }
- }
- }
代碼分步解釋:
一旦使用限制當(dāng)前線程運(yùn)行的同步式dispatch_group_wait,你必須用dispatch_async將整個(gè)方法調(diào)至后臺(tái)隊(duì)列以保證主線程的正常運(yùn)行。
在這里聲稱了一個(gè)你可以將其視為未完成任務(wù)計(jì)數(shù)器的新派發(fā)組。
dispatch_group_enter用來(lái)向派發(fā)組提醒新任務(wù)執(zhí)行的開(kāi)始。你必須使調(diào)用dispatch_group_enter的次數(shù)要相稱于調(diào)用dispatch_group_leave的次數(shù),不然將會(huì)導(dǎo)致App崩潰。
在這里,你向派發(fā)組提醒任務(wù)的執(zhí)行結(jié)束。再?gòu)?qiáng)調(diào)一遍,進(jìn)出派發(fā)組的次數(shù)一定要相等。
在所有任務(wù)執(zhí)行結(jié)束后或者在處理超時(shí)的情況下dispatch_group_wait才會(huì)執(zhí)行。假如在所有任務(wù)執(zhí)行結(jié)束前就出現(xiàn)了處理超時(shí)的情況,函數(shù)便會(huì)返回一個(gè)非零結(jié)果。你可以將其放在一個(gè)特殊的閉包中以檢查是否會(huì)發(fā)生處理超時(shí)的情況。當(dāng)然,在本教程的情況下你可以使用DISPATCH_TIME_FOREVER令其保持等待請(qǐng)求狀態(tài),這就意味它會(huì)一直等下去,因?yàn)檎掌南螺d任務(wù)總會(huì)完成的。
到目前為止,你保證了照片下載任務(wù)要么順利完成要么出現(xiàn)處理超時(shí)的情況。其后你便可以返回至主隊(duì)列運(yùn)行你的completion閉包。這將向主線程添加稍后將被執(zhí)行的任務(wù)。
條件允許的情況下執(zhí)行completion閉包。
編譯并運(yùn)行你的App,你會(huì)發(fā)現(xiàn)在點(diǎn)擊下載照片的選項(xiàng)后你的completion閉包將會(huì)在正確的時(shí)間執(zhí)行。
提醒:當(dāng)你在實(shí)體設(shè)備上運(yùn)行App的時(shí)候,假如網(wǎng)絡(luò)機(jī)制運(yùn)行過(guò)快以至于你無(wú)法判斷的completion閉包開(kāi)始執(zhí)行時(shí)間的話,你可以到App的Setting中的Developer Section中進(jìn)行一些網(wǎng)絡(luò)調(diào)整。打開(kāi)Network Link Conditioner,選擇Very Bad Network是一個(gè)不錯(cuò)的選擇。
假如你在模擬器上運(yùn)行App的話,你可以通過(guò)使用Network Link Conditioner included in the Hardare IO Tools for Xcode調(diào)整你的網(wǎng)絡(luò)速度。這是一個(gè)當(dāng)你需要了解在網(wǎng)絡(luò)狀況不好的情況下App執(zhí)行情況的***工具。
這個(gè)解決方法的好處不止于此,但總體上來(lái)說(shuō)它在大多數(shù)情況下避免了限制線程正常運(yùn)行的可能。你接下來(lái)的任務(wù)便是寫一個(gè)相同的并以異步的方式向你發(fā)出'照片下載完成'提醒的方法。
在開(kāi)始之前先了解一下對(duì)于不同類型的隊(duì)列來(lái)說(shuō)應(yīng)該何時(shí)使用并怎樣使用派發(fā)組的簡(jiǎn)短教程。
自定義連續(xù)隊(duì)列(Custom Serial Queue): 在組內(nèi)任務(wù)全部完成時(shí)需要發(fā)出提醒的情況下,自定義連續(xù)隊(duì)列是一個(gè)不錯(cuò)的選擇。
主隊(duì)列(Main Queue[Serial]):在當(dāng)你以同步的方式等待所有任務(wù)的完成且你也不想限制主隊(duì)列的運(yùn)行的情況下你應(yīng)該在主線程上警惕使用它。但比如像網(wǎng)絡(luò)請(qǐng)求這種長(zhǎng)時(shí)間運(yùn)行的任務(wù)結(jié)束時(shí)異步模型是用來(lái)更新UI的***方式。
并發(fā)隊(duì)列(Concurrent Queue):這對(duì)于派發(fā)組及完成提醒也是個(gè)不錯(cuò)的選擇。
派發(fā)組,再來(lái)一次!
出于精益求精的目的,通過(guò)異步的方式將下載任務(wù)派發(fā)到另一個(gè)隊(duì)列并用dispatch_group_wait限制其運(yùn)行的做法是不是有些stupid呢?試試另一種方法吧...
用如下實(shí)現(xiàn)代碼代替PhtotManager.swift中的downloadPhotosWithCompletion函數(shù):
- 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)
- }
- }
- }
這就是你的新異步方法的工作原理:
在這個(gè)新的實(shí)現(xiàn)方法中,當(dāng)你不再限制主線程的時(shí)候你就沒(méi)有必要將其放進(jìn)dispatch_async的調(diào)用中。
dispatch_group_notify相當(dāng)于一個(gè)異步completion閉包。當(dāng)派發(fā)組中不再剩余任何任務(wù)且輪到completion閉包運(yùn)行時(shí),這段代碼將會(huì)執(zhí)行。你也可以定義你的completion代碼在哪個(gè)隊(duì)列上運(yùn)行。在這段代碼中你便要運(yùn)行在主隊(duì)列上。
對(duì)于在不限制任何線程運(yùn)行的情況下處理這種特殊需求的例子來(lái)說(shuō),這是一種較為簡(jiǎn)潔的方式。
過(guò)多使用并發(fā)機(jī)制造成的危險(xiǎn)
學(xué)了這么多的新東西后,你是不是該令你的代碼全部實(shí)現(xiàn)線程化呢?
Do It !!!
看看你在PhotoManger中的downloadPhotosWithCompletion函數(shù)。你應(yīng)該注意到了那個(gè)循環(huán)在三個(gè)參數(shù)間并下載三張不同照片的for循環(huán)。你接下來(lái)的工作便是嘗試通過(guò)并發(fā)機(jī)制加快for循環(huán)的運(yùn)行速度。
該輪到dispatch_apply上場(chǎng)了。
dispatch_apply就像是一個(gè)以并發(fā)的形式執(zhí)行不同迭代過(guò)程的for循環(huán)。就像是一個(gè)普通的for循環(huán),dispatch_apply是一個(gè)同步運(yùn)行且所有工作完成后才會(huì)返回的函數(shù)。
當(dāng)你在對(duì)閉包內(nèi)已給定任務(wù)的數(shù)量進(jìn)行***化迭代過(guò)程數(shù)量的設(shè)定時(shí)一定要當(dāng)心,因?yàn)檫@種存在多個(gè)迭代過(guò)程且每個(gè)迭代過(guò)程僅包含少量工作的情況所消耗的資源會(huì)抵消掉并發(fā)調(diào)用所產(chǎn)生的優(yōu)化效果。這個(gè)叫做striding的技術(shù)會(huì)在你處理多任務(wù)的每個(gè)迭代過(guò)程的地方幫到你。
什么時(shí)候適合用dispatch_apply呢?
自定義連續(xù)隊(duì)列(Custome Serial Queue):對(duì)于連續(xù)隊(duì)列來(lái)說(shuō),dispatch_apply沒(méi)什么用處;你還是老實(shí)地用普通的for循環(huán)吧。
主隊(duì)列(Main Queue[Serial]):跟上面情況一樣,老實(shí)地用普通for循環(huán)。
并發(fā)隊(duì)列(Concurrent Queue):當(dāng)你需要監(jiān)管你的任務(wù)處理進(jìn)程時(shí),并發(fā)循環(huán)絕對(duì)是一個(gè)好主意。
回到downloadPhotosWithCompletion并替換成如下代碼:
- func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
- var storedError: NSError!
- var downloadGroup = dispatch_group_create()
- let addresses = [OverlyAttachedGirlfriendURLString,
- SuccessKidURLString,
- LotsOfFacesURLString]
- dispatch_apply(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)
- }
- }
- }
你的循環(huán)塊現(xiàn)在就是以并發(fā)的形式運(yùn)行;在上述代碼中,你為調(diào)用dispatch+apply提供了三個(gè)參數(shù)。***參數(shù)聲明了迭代過(guò)程的數(shù)量,第二個(gè)參數(shù)聲明了將要運(yùn)行多個(gè)任務(wù)的隊(duì)列,第三個(gè)參數(shù)聲明了閉包。
要知道盡管你已經(jīng)有了在線程安全模式下添加照片的代碼,但是照片順序會(huì)根據(jù)***完成的線程的順序所排列。
編譯并運(yùn)行,通過(guò)Le Internet的方式添加一些照片。注意到有什么不同嗎?
在真實(shí)設(shè)備上運(yùn)行修改后的代碼偶爾會(huì)運(yùn)行得快一些。所以,上面做出的修改值得嗎?
其實(shí),在這種情況下它不值得你這么做。原因如下:
你已經(jīng)調(diào)用出了比f(wàn)or循環(huán)再同種情況下消耗更多資源的線程。dispatch_apply在這里顯得有些小題大做了。
你寫App的時(shí)間是有限的--不要為那些'抓雞不成蝕把米'的優(yōu)化代碼浪費(fèi)時(shí)間,把你的時(shí)間用在優(yōu)化得有明顯效果的代碼上。你可以選擇使用Xcode中的Instruments來(lái)測(cè)試出你App中執(zhí)行時(shí)間最長(zhǎng)的方法。
在某些情況下,優(yōu)化后的代碼甚至?xí)黾幽愫推渌_(kāi)發(fā)者理解其邏輯結(jié)構(gòu)的難度,所以優(yōu)化效果一定要是物有所值的。
記住,不要癡迷于優(yōu)化,要不然你就是和自己過(guò)不去了。
取消派發(fā)塊(dispatch block)的執(zhí)行
要知道在iOS 8和OS X Yosemite中加入了名為dispatch block objects的新功能(中文叫‘派發(fā)塊對(duì)象’感覺(jué)總是怪怪的,所以就繼續(xù)用英文原名)。Dispatch block objects可以做不少事兒了,比如通過(guò)為每個(gè)對(duì)象設(shè)定一個(gè)QoS等級(jí)來(lái)決定其在隊(duì)列中的優(yōu)先級(jí),但它最特別的功能便是取消block objects的執(zhí)行。但你需要知道的是一個(gè)block object只有在到達(dá)隊(duì)列頂端且開(kāi)始執(zhí)行前才能被取消。
咱們可以通過(guò)‘利用Le Internet開(kāi)始并再取消照片下載任務(wù)’的方式詳細(xì)描述取消Dispatch Block的運(yùn)行機(jī)制。用下述代碼代替PhotoManager.swift中的downloadPhotosWithCompletion函數(shù):
- 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)
- }
- }
- }
addresses數(shù)組內(nèi)包含每個(gè)都被復(fù)制了三份的address變量。
這個(gè)數(shù)組包含著晚些時(shí)候?qū)⒈皇褂玫腷lock objects。
dispatch_block_create聲明了一個(gè)新的block object。***個(gè)參數(shù)是定義了不同block特性的標(biāo)志。這個(gè)標(biāo)志使得block繼承了其在被分配至隊(duì)列中的QoS等級(jí)。第二個(gè)參數(shù)是以一個(gè)閉包形式定義的block。
這是一個(gè)以異步形式分發(fā)到全局主隊(duì)列的block。在這個(gè)例子中所使用的主隊(duì)列是一個(gè)連續(xù)隊(duì)列,所以其更容易取消所選的blocks。定義了分發(fā)blocks的代碼已在主隊(duì)列上的執(zhí)行保證了下載blocks的稍后執(zhí)行。
除去前三次的下載,在剩余的數(shù)組元素中執(zhí)行for循環(huán)。
arc4random_uniform提供一個(gè)在0至上限范圍內(nèi)(不包含上限)的整數(shù)。就像擲硬幣那樣,將2設(shè)定為上限后你將會(huì)得到0或1中的某一個(gè)整數(shù)。
假如在隨機(jī)數(shù)是1、block還在隊(duì)列中且沒(méi)有正在被執(zhí)行的情況下,block則被取消。在執(zhí)行過(guò)程中的block是不能被取消的。
當(dāng)所有blocks都被加入分發(fā)隊(duì)列后,不要忘記刪除被取消的隊(duì)列。
編譯并運(yùn)行App,通過(guò)Le Internet 的方式添加照片。你會(huì)發(fā)現(xiàn)App在下載了原來(lái)的三張照片后還會(huì)再下載一個(gè)隨機(jī)數(shù)量的照片。分配至隊(duì)列后,剩余的blocks將被取消。盡管這是一個(gè)很牽強(qiáng)的例子但起碼它很好的描述了dispatch block objects如何被使用或被取消的。
Dispatch block objects能做的還有很多,使用前別忘了看看官方文檔。
GCD帶來(lái)的各種各樣的樂(lè)趣
且慢!再容我講點(diǎn)兒東西!其實(shí)這還有些不常用的函數(shù),但在特殊情況下它們是非常有用的。
測(cè)試異步代碼
這也許聽(tīng)起來(lái)有些不靠譜,但你知道Xcode上的確有這項(xiàng)測(cè)試功能嗎?:]其實(shí)在某些情況下我是假裝不知道有這項(xiàng)功能的,但是在處理具有復(fù)雜關(guān)系的代碼的時(shí)候,代碼編寫與運(yùn)行的測(cè)試是非常重要的。
Xcode中的測(cè)試功能是以XCTestCase的子類形式出現(xiàn)其且在其中運(yùn)行的任何方法都是以test開(kāi)頭出現(xiàn)的。測(cè)試功能在主線程上運(yùn)行,所以你可以假設(shè)每個(gè)測(cè)試都是以一種連續(xù)(serial)的方式運(yùn)行的。
只要一個(gè)給定的測(cè)試方法完成了執(zhí)行,XCTest方法就會(huì)認(rèn)定一個(gè)測(cè)試已經(jīng)完成并開(kāi)始下一個(gè)測(cè)試,這就意味著在新的測(cè)試運(yùn)行的同時(shí),上一個(gè)測(cè)試中的異步代碼還會(huì)繼續(xù)運(yùn)行。
當(dāng)你在執(zhí)行一個(gè)網(wǎng)絡(luò)請(qǐng)求任務(wù)且不想限制主線程的運(yùn)行時(shí),那么這類網(wǎng)絡(luò)任務(wù)通常是以異步方式執(zhí)行的。這種“測(cè)試方法的結(jié)束代表著整個(gè)測(cè)試過(guò)程的結(jié)束”的機(jī)制加大了網(wǎng)絡(luò)代碼測(cè)試的難度。
別緊張,接下來(lái)咱們看兩個(gè)常用的且專門用來(lái)測(cè)試以異步方式執(zhí)行的代碼的技術(shù):一個(gè)使用了信號(hào)量(semaphores),另一個(gè)使用了期望(expectations)。
信號(hào)量(Semaphores)
在很多學(xué)校的OS課中,一提到大名鼎鼎的Edsger W.Dijkstra時(shí)肯定會(huì)講到信號(hào)量這個(gè)跟線程相關(guān)的概念。信號(hào)量難懂之處在于它建立在那些復(fù)雜的操作系統(tǒng)的函數(shù)之上。
假如你想學(xué)習(xí)更多關(guān)于信號(hào)量的知識(shí),請(qǐng)到這里了解更多關(guān)于信號(hào)量理論的細(xì)節(jié)。假如你是個(gè)專注于學(xué)術(shù)研究的家伙,從軟件開(kāi)發(fā)的角度來(lái)看,關(guān)于信號(hào)量的經(jīng)典例子肯定就是哲學(xué)家進(jìn)餐問(wèn)題了。
信號(hào)量適用于讓你在資源有限的情況下控制多個(gè)單位的資源消耗。舉個(gè)例子,假如你聲明了一個(gè)其中包含兩個(gè)資源的信號(hào)量,在同一時(shí)間內(nèi)最多只能有兩個(gè)線程訪問(wèn)臨界區(qū)。其他想使用資源的線程必須以FIFO(First Come, First Operate)的順序在隊(duì)列中等待。
打開(kāi)GooglyPuffTests.swift并用如下代碼替換downloadImageURLWithString函數(shù):
- 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)量如何在上述代碼中工作的解釋:
創(chuàng)建信號(hào)量。參數(shù)表明了信號(hào)量的初始值。這個(gè)數(shù)字代表著可以訪問(wèn)信號(hào)量線程的數(shù)量,我們經(jīng)常以發(fā)送信號(hào)的方式來(lái)增加信號(hào)量。
你可以在completion閉包中向信號(hào)量聲明你不再需要資源了。這樣的話,信號(hào)量的值會(huì)得到增加并且向其他資源聲明此時(shí)信號(hào)量可用。
設(shè)定信號(hào)量請(qǐng)求超時(shí)的時(shí)間。在信號(hào)量聲明可用前,當(dāng)前線程的運(yùn)行將被限制。若出現(xiàn)超時(shí)的話,函數(shù)將會(huì)返回一個(gè)非零的值。在這種情況下測(cè)試便是失敗的,因?yàn)樗J(rèn)為網(wǎng)絡(luò)請(qǐng)求的返回不該使用超過(guò)十秒的時(shí)間。
通過(guò)使用菜單中的Product/Test選項(xiàng)或者使用?+U快捷鍵測(cè)試App的運(yùn)行。
斷開(kāi)網(wǎng)絡(luò)連接后再次運(yùn)行測(cè)試;假如你在實(shí)機(jī)上運(yùn)行就打開(kāi)飛行模式,若是模擬器的話就斷開(kāi)鏈接。10秒后這次測(cè)試便以失敗告終。
假如你是一個(gè)服務(wù)器團(tuán)隊(duì)中一員,完成這些測(cè)試還是挺重要的。
期望(Expectations)
XCTest框架提供了另一個(gè)用來(lái)測(cè)試異步方式執(zhí)行代碼的解決方案,期望。這便允許你在一個(gè)異步任務(wù)開(kāi)始執(zhí)行前設(shè)定一個(gè)期望--一些你期待發(fā)生的事。在異步任務(wù)的期望被標(biāo)記為已完成(fulfilled)前,你可以令test runner一直保持等待狀態(tài)。
用以下代碼代替GooglyPufftests.swift中的downloadImageWithString函數(shù):
- 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)
- }
- }
- }
解釋一下:
通過(guò)expectationWithDescription參數(shù)聲明了一個(gè)期望。當(dāng)測(cè)試失敗的時(shí)候,test runner將會(huì)在Log中顯示這段字符串參數(shù),以此代表著你所期待發(fā)生的事情。
調(diào)用以異步方式標(biāo)記期望已完成的閉包中的fulfill.
調(diào)用的線程等待期望被waitForExpectationsWithTimeout函數(shù)標(biāo)記完成。若等待超時(shí),線程將被當(dāng)做一個(gè)錯(cuò)誤。
編譯并運(yùn)行測(cè)試。盡管測(cè)試結(jié)果跟使用信號(hào)量機(jī)制比起來(lái)并沒(méi)有太多的不同,但這卻是一種使XCTest框架更加簡(jiǎn)潔易讀的方法。
派發(fā)源(Dispatch Sources)的使用
GCD的一個(gè)非常有趣的特性就是派發(fā)源,它是包含了很多低層級(jí)別的功能。這些功能可以幫你對(duì)Unix的信號(hào),文件描述符,Mach端口、VFS Nodes進(jìn)行反饋以及檢測(cè)。盡管這些東西超出了這篇教程的范圍,但我覺(jué)得你還是要試著去實(shí)現(xiàn)一個(gè)派發(fā)源對(duì)象。
很多派發(fā)源的初學(xué)者經(jīng)常被卡在如何使用一個(gè)源的的問(wèn)題上,所以你要清楚dispatch_source_create的工作原理。下面的函數(shù)聲明了一個(gè)源:
- 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決定了接下來(lái)的句炳以及掩碼參數(shù)的類型。你可以去看一下相關(guān)內(nèi)容的官方文檔以便理解每一種dispatch_source_type_t的用法與解釋。
在這里你將監(jiān)管DISPATCH_SOURCE_TYPE_SIGNAL。如官方文檔里解釋的那樣:派發(fā)源監(jiān)管著當(dāng)前進(jìn)程的信號(hào),其句炳的值是一個(gè)整數(shù)(int),而掩碼值暫時(shí)沒(méi)有用到而被設(shè)為0。
這些Unix信號(hào)可以在名為signal.h的頭文件中找到。文件的頂端有很多的#defines。在這堆信號(hào)中你將對(duì)SIGSTOP信號(hào)進(jìn)行監(jiān)管。當(dāng)進(jìn)程收到一個(gè)不可避免的暫停掛起指令時(shí)這個(gè)信號(hào)將被發(fā)送。當(dāng)你用LLDB的debugger除錯(cuò)的時(shí)候同樣的信號(hào)也會(huì)被發(fā)送。
打開(kāi)PhotoCollectionViewController.swift文件,在viewDidLoad函數(shù)附近添加如下代碼:
- #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
- }
分布解釋如下:
從安全角度考慮,你***在DEBUG模式下編譯代碼,以防其他人間接地看到你的代碼。: ] 通過(guò)在以下路徑,Project Settings -> Build Settings -> Swift Compiler – Custom Flags -> Other Swift Flags -> Debug,通過(guò)在Debug選項(xiàng)中添加-D DEBUG的方式來(lái)定義DEBUG。
利用dispatch_once對(duì)派發(fā)源進(jìn)行單次的初始化設(shè)定。
在這里你實(shí)例化了一個(gè)signalSource變量并表明你只想進(jìn)行信號(hào)監(jiān)管并將SIGSTOP信號(hào)作為第二個(gè)參數(shù)。再有一點(diǎn)你需要知道的是你使用主隊(duì)列處理接收到的事件--至于為什么,你待會(huì)兒就會(huì)知道了。
假如你提供了一個(gè)錯(cuò)誤的參數(shù),派發(fā)源對(duì)象將不會(huì)被創(chuàng)建成功。總之,在使用派發(fā)源對(duì)象前你要確定你已經(jīng)創(chuàng)建了一個(gè)可用的派發(fā)源對(duì)象。
dispatch_source_set_event_handler注冊(cè)了一個(gè)在收到特定任務(wù)信號(hào)時(shí)將被調(diào)用的事件處理閉包。
在默認(rèn)的情況下,所有派發(fā)源都是在暫停掛起的狀態(tài)下開(kāi)始執(zhí)行的。當(dāng)你打算開(kāi)始監(jiān)管事件時(shí),你必須讓派發(fā)源對(duì)象從新開(kāi)始運(yùn)行。
編譯并運(yùn)行App;以debugger方式暫停App的運(yùn)行并再立刻恢復(fù)運(yùn)行。檢查一下控制臺(tái),你會(huì)看到如下的反饋:
- 2014-08-12 12:24:00.514 GooglyPuff[24985:5481978] Hi, I am:
在某種程度上你的App現(xiàn)在知道debug了。這非常不錯(cuò),但是如何較為實(shí)際地使用它呢?
當(dāng)你從新恢復(fù)App運(yùn)行時(shí)你可以使用它對(duì)一個(gè)object進(jìn)行debug并顯示其相關(guān)數(shù)據(jù);當(dāng)某些不懷好意的人想利用debugger影響App正常運(yùn)行的時(shí)候,你也可以為你的App寫一個(gè)自定義安全登錄模塊。
另一個(gè)有趣的想法便是通過(guò)上述機(jī)制實(shí)現(xiàn)一個(gè)對(duì)于debugger中對(duì)象的堆棧跟蹤器。
What ?
考慮一下這種情況,你意外的停止了debugger的運(yùn)行并在很大程度上debugger很難待在預(yù)計(jì)的棧幀上。但現(xiàn)在你可以任何時(shí)間停止debugger的運(yùn)行并任何地方執(zhí)行代碼。假如你想在App的特定位置中執(zhí)行代碼,上述情況將會(huì)非常有用。
在viewDidLoad的事件處理閉包中的NSLog代碼旁添加斷點(diǎn)。在debugger中暫停運(yùn)行,在恢復(fù)運(yùn)行;你的App將運(yùn)行至斷點(diǎn)添加處。現(xiàn)在你便可以隨心所欲地訪問(wèn)PhotoCollectionViewController中的實(shí)例了。
假如你不知道debugger中有哪些線程,可以去查看一下。主線程總會(huì)是***個(gè)被libdispatch跟隨的線程;GCD的協(xié)調(diào)器總會(huì)是第二個(gè)線程;其他的線程就要視具體情況而定了。
利用斷點(diǎn)功能你可以逐步地更新UI、測(cè)試類的屬性甚至在不重新運(yùn)行App的情況下執(zhí)行特定的方法,看起來(lái)很方便吧!
Where to Go From Here?
你可以在這里下載到教程的完整代碼。
我挺不喜歡嘮叨的,但是我覺(jué)得你還是應(yīng)該去看看這篇關(guān)于如何使用Xcode中的Instruments的教程。假如你打算對(duì)你的App進(jìn)行優(yōu)化的話,你是絕對(duì)會(huì)用到Instruments的。要知道Instruments對(duì)于推斷相對(duì)執(zhí)行的問(wèn)題是很有用處的:對(duì)比不同代碼塊中哪一塊的相對(duì)執(zhí)行時(shí)間是最長(zhǎng)的。
與此同時(shí),你也有必要去看看這篇How to Use NSOperations and NSOperationsQueue Tutorial in Swift的教程。NSOperations可以提供更良好的控制,實(shí)現(xiàn)多并發(fā)任務(wù)***數(shù)量的處理以及在犧牲一定運(yùn)行速度的情況下使得程序更加面向?qū)ο蠡?/p>
記??!在大多數(shù)情況下,除非你有著特殊的原因,一定要盡量使用更高級(jí)別的API。只有當(dāng)你真的想到學(xué)到或做到一些非常有趣的事情時(shí)再去探索蘋果的Dark Arts吧!
祝你好運(yùn)!