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