本文翻譯自 http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1html
原做者:Derek Selanderios
譯者:@nixzhugit
雖然 GCD 已經出現過一段時間了,但不是每一個人都明瞭其主要內容。這是能夠理解的;併發一直很棘手,而 GCD 是基於 C 的 API ,它們就像一組尖銳的棱角戳進 Objective-C 的平滑世界。咱們將分兩個部分的教程來深刻學習 GCD 。github
在這兩部分的系列中,第一個部分的將解釋 GCD 是作什麼的,並從許多基本的 GCD 函數中找出幾個來展現。在第二部分,你將學到幾個 GCD 提供的高級函數。vim
GCD 是 libdispatch
的市場名稱,而 libdispatch 做爲 Apple 的一個庫,爲併發代碼在多核硬件(跑 iOS 或 OS X )上執行提供有力支持。它具備如下優勢:數組
本教程假設你對 Block 和 GCD 有基礎瞭解。若是你對 GCD 徹底陌生,先看看 iOS 上的多線程和 GCD 入門教程 學習其要領。xcode
要理解 GCD ,你要先熟悉與線程和併發相關的幾個概念。這二者均可能模糊和微妙,因此在開始 GCD 以前先簡要地回顧一下它們。安全
這些術語描述當任務相對於其它任務被執行,任務串行執行就是每次只有一個任務被執行,任務併發執行就是在同一時間能夠有多個任務被執行。服務器
雖然這些術語被普遍使用,本教程中你能夠將任務設定爲一個 Objective-C 的 Block 。不明白什麼是 Block ?看看 iOS 5 教程中的如何使用 Block 。實際上,你也能夠在 GCD 上使用函數指針,但在大多數場景中,這實際上更難於使用。Block 就是更加容易些!網絡
在 GCD 中,這些術語描述當一個函數相對於另外一個任務完成,此任務是該函數要求 GCD 執行的。一個同步函數只在完成了它預約的任務後才返回。
一個異步函數,恰好相反,會當即返回,預約的任務會完成但不會等它完成。所以,一個異步函數不會阻塞當前線程去執行下一個函數。
注意——當你讀到同步函數「阻塞(Block)」當前線程,或函數是一個「阻塞」函數或阻塞操做時,不要被搞糊塗了!動詞「阻塞」描述了函數如何影響它所在的線程而與名詞「代碼塊(Block)」沒有關係。代碼塊描述了用 Objective-C 編寫的一個匿名函數,它能定義一個任務並被提交到 GCD 。
譯者注:中文不會有這個問題,「阻塞」和「代碼塊」是兩個詞。
就是一段代碼不能被併發執行,也就是,兩個線程不能同時執行這段代碼。這很常見,由於代碼去操做一個共享資源,例如一個變量若能被併發進程訪問,那麼它極可能會變質(譯者注:它的值再也不可信)。
這種情況是指基於特定序列或時機的事件的軟件系統以不受控制的方式運行的行爲,例如程序的併發任務執行的確切順序。競態條件可致使沒法預測的行爲,而不能經過代碼檢查當即發現。
兩個(有時更多)東西——在大多數狀況下,是線程——所謂的死鎖是指它們都卡住了,並等待對方完成或執行其它操做。第一個不能完成是由於它在等待第二個的完成。但第二個也不能完成,由於它在等待第一個的完成。
線程安全的代碼能在多線程或併發任務中被安全的調用,而不會致使任何問題(數據損壞,崩潰,等)。線程不安全的代碼在某個時刻只能在一個上下文中運行。一個線程安全代碼的例子是 NSDictionary
。你能夠在同一時間在多個線程中使用它而不會有問題。另外一方面,NSMutableDictionary
就不是線程安全的,應該保證一次只能有一個線程訪問它。
一個上下文切換指當你在單個進程裏切換執行不一樣的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很廣泛,但會帶來一些額外的開銷。
併發和並行一般被一塊兒提到,因此值得花些時間解釋它們之間的區別。
併發代碼的不一樣部分能夠「同步」執行。然而,該怎樣發生或是否發生都取決於系統。多核設備經過並行來同時執行多個線程;然而,爲了使單核設備也能實現這一點,它們必須先運行一個線程,執行一個上下文切換,而後運行另外一個線程或進程。這一般發生地足夠快以至給咱們併發執行地錯覺,以下圖所示:
雖然你能夠編寫代碼在 GCD 下併發執行,但 GCD 會決定有多少並行的需求。並行要求併發,但併發並不能保證並行。
更深刻的觀點是併發其實是關於構造。當你在腦海中用 GCD 編寫代碼,你組織你的代碼來暴露能同時運行的多個工做片斷,以及不能同時運行的那些。若是你想深刻此主題,看看 這個由Rob Pike作的精彩的講座 。
GCD 提供有 dispatch queues
來處理代碼塊,這些隊列管理你提供給 GCD 的任務並用 FIFO 順序執行這些任務。這就保證了第一個被添加到隊列裏的任務會是隊列中第一個開始的任務,而第二個被添加的任務將第二個開始,如此直到隊列的終點。
全部的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程並行的訪問它們。當你瞭解了調度隊列如何爲你本身代碼的不一樣部分提供線程安全後,GCD的優勢就是顯而易見的。關於這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工做。
在本節你會看到兩種調度隊列,都是由 GCD 提供的,而後看一些描述如何用調度函數添加工做到隊列的例子。
串行隊列中的任務一次執行一個,每一個任務只在前一個任務完成時纔開始。並且,你不知道在一個 Block 結束和下一個開始之間的時間長度,以下圖所示:
這些任務的執行時機受到 GCD 的控制;惟一能確保的事情是 GCD 一次只執行一個任務,而且按照咱們添加到隊列的順序來執行。
因爲在串行隊列中不會有兩個任務併發運行,所以不會出現同時訪問臨界區的風險;相對於這些任務來講,這就從競態條件下保護了臨界區。因此若是訪問臨界區的惟一方式是經過提交到調度隊列的任務,那麼你就不須要擔憂臨界區的安全問題了。
在併發隊列中的任務能獲得的保證是它們會按照被添加的順序開始執行,但這就是所有的保證了。任務可能以任意順序完成,你不會知道什麼時候開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這徹底取決於 GCD 。
下圖展現了一個示例任務執行計劃,GCD 管理着四個併發任務:
注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始後,Block 1等待了好一下子纔開始。一樣, Block 3 在 Block 2 以後纔開始,但它先於 Block 2 完成。
什麼時候開始一個 Block 徹底取決於 GCD 。若是一個 Block 的執行時間與另外一個重疊,也是由 GCD 來決定是否將其運行在另外一個不一樣的核心上,若是那個核心可用,不然就用上下文切換的方式來執行不一樣的 Block 。
有趣的是, GCD 提供給你至少五個特定的隊列,可根據隊列類型選擇使用。
首先,系統提供給你一個叫作 主隊列(main queue)
的特殊隊列。和其它串行隊列同樣,這個隊列中的任務一次只能執行一個。然而,它能保證全部的任務都在主線程執行,而主線程是惟一可用於更新 UI 的線程。這個隊列就是用於發生消息給 UIView
或發送通知的。
系統同時提供給你好幾個併發隊列。它們叫作 全局調度隊列(Global Dispatch Queues)
。目前的四個全局隊列有着不一樣的優先級:background
、low
、default
以及 high
。要知道,Apple 的 API 也會使用這些隊列,因此你添加的任何任務都不會是這些隊列中惟一的任務。
最後,你也能夠建立本身的串行隊列或併發隊列。這就是說,至少有五個隊列任你處置:主隊列、四個全局調度隊列,再加上任何你本身建立的隊列。
以上是調度隊列的大框架!
GCD 的「藝術」歸結爲選擇合適的隊列來調度函數以提交你的工做。體驗這一點的最好方式是走一遍下邊的列子,咱們沿途會提供一些通常性的建議。
既然本教程的目標是優化且安全的使用 GCD 調用來自不一樣線程的代碼,那麼你將從一個近乎完成的叫作 GooglyPuff
的項目入手。
GooglyPuff 是一個沒有優化,線程不安全的應用,它使用 Core Image 的人臉檢測 API 來覆蓋一對曲棍球眼睛到被檢測到的人臉上。對於基本的圖像,能夠從相機膠捲選擇,或用預設好的URL從互聯網下載。
完成項目下載以後,將其解壓到某個方便的目錄,再用 Xcode 打開它並編譯運行。這個應用看起來以下圖所示:
注意當你選擇 Le Internet
選項下載圖片時,一個 UIAlertView
過早地彈出。你將在本系列教程地第二部分修復這個問題。
這個項目中有四個有趣的類:
NSURL
的實例或一個 ALAsset
的實例來實例化照片。這個類提供一個圖像、縮略圖以及從 URL 下載的狀態。Photo
的實例.回到應用並從你的相機膠捲添加一些照片或使用 Le Internet
選項下載一些。
注意在按下 PhotoCollectionViewController
中的一個 UICollectionViewCell
到生成一個新的 PhotoDetailViewController
之間花了多久時間;你會注意到一個明顯的滯後,特別是在比較慢的設備上查看很大的圖。
在重載 UIViewController 的 viewDidLoad
時容易加入太多雜亂的工做(too much clutter),這一般會引發視圖控制器出現前更長的等待。若是可能,最好是卸下一些工做放到後臺,若是它們不是絕對必需要運行在加載時間裏。
這聽起來像是 dispatch_async
能作的事情!
打開 PhotoDetailViewController
並用下面的實現替換 viewDidLoad
:
- (void)viewDidLoad
{
[super viewDidLoad]; NSAssert(_image, @"Image not set; required to use view controller"); self.photoImageView.image = _image; //Resize if neccessary to ensure it's not pixelated if (_image.size.height <= self.photoImageView.bounds.size.height && _image.size.width <= self.photoImageView.bounds.size.width) { [self.photoImageView setContentMode:UIViewContentModeCenter]; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; dispatch_async(dispatch_get_main_queue(), ^{ // 2 [self fadeInNewImage:overlayImage]; // 3 }); }); }
下面來講明上面的新代碼所作的事:
dispatch_async()
,Block 會被異步地提交,意味着調用線程地執行將會繼續。這就使得 viewDidLoad
更早地在主線程完成,讓加載過程感受起來更加快速。同時,一我的臉檢測過程會啓動並將在稍後完成。UIImageView
,那麼你就添加一個新的 Block 到主線程。記住——你必須老是在主線程訪問 UIKit 的類。fadeInNewImage:
更新 UI ,它執行一個淡入過程切換到新的曲棍球眼睛圖像。編譯並運行你的應用;選擇一個圖像而後你會注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在以後就加上了。這給應用帶來了不錯的效果,和以前的顯示差異巨大。
進一步,若是你試着加載一個超大的圖像,應用不會在加載視圖控制器上「掛住」,這就使得應用具備很好伸縮性。
正如以前提到的, dispatch_async
添加一個 Block 到隊列就當即返回了。任務會在以後由 GCD 決定執行。當你須要在後臺執行一個基於網絡或 CPU 緊張的任務時就使用 dispatch_async
,這樣就不會阻塞當前線程。
下面是一個關於在 dispatch_async
上如何以及什麼時候使用不一樣的隊列類型的快速指導:
dispatch_sync
。dispatch_async
到主隊列,你能確保這個新任務將在當前方法完成後的某個時間執行。稍微考慮一下應用的 UX 。是否用戶第一次打開應用時會困惑於不知道作什麼?你是這樣嗎? :]
若是用戶的 PhotoManager
裏尚未任何照片,那麼顯示一個提示會是個好主意!然而,你一樣要考慮用戶的眼睛會如何在主屏幕上瀏覽:若是你太快的顯示一個提示,他們的眼睛還徘徊在視圖的其它部分上,他們極可能會錯過它。
顯示提示以前延遲一秒鐘就足夠捕捉到用戶的注意,他們此時已經第一次看過了應用。
添加以下代碼到到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的廢止實現裏:
- (void)showOrHideNavPrompt
{
NSUInteger count = [[PhotoManager sharedManager] photos].count; double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 if (!count) { [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; } else { [self.navigationItem setPrompt:nil]; } }); }
showOrHideNavPrompt 在 viewDidLoad 中執行,以及 UICollectionView 被從新加載的任什麼時候候。按照註釋數字順序看看:
delayInSeconds
給定的時長,再異步地添加一個 Block 到主線程。編譯並運行應用。應該有一個輕微地延遲,這有助於抓住用戶的注意力並展現所要作的事情。
dispatch_after
工做起來就像一個延遲版的 dispatch_async
。你依然不能控制實際的執行時間,且一旦 dispatch_after
返回也就不能再取消它。
不知道什麼時候適合使用 dispatch_after
?
dispatch_after
要當心。你最好堅持使用主隊列。dispatch_after
的好選擇;Xcode 提供了一個不錯的自動完成模版。dispatch_after
也要當心;你會這樣作就比較罕見。仍是在主隊列作這些操做吧。單例,不論喜歡仍是討厭,它們在 iOS 上的流行狀況就像網上的貓。 :]
一個常見的擔心是它們經常不是線程安全的。這個擔心十分合理,基於它們的用途:單例經常被多個控制器同時訪問。
單例的線程擔心範圍從初始化開始,到信息的讀和寫。PhotoManager
類被實現爲單例——它在目前的狀態下就會被這些問題所困擾。要看看事情如何很快地失去控制,你將在單例實例上建立一個控制好的競態條件。
導航到 PhotoManager.m
並找到 sharedManager
;它看起來以下:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
當前狀態下,代碼至關簡單;你建立了一個單例並初始化一個叫作 photosArray
的 NSMutableArray
屬性。
然而,if
條件分支不是線程安全的;若是你屢次調用這個方法,有一個可能性是在某個線程(就叫它線程A)上進入 if
語句塊並可能在 sharedPhotoManager
被分配內存前發生一個上下文切換。而後另外一個線程(線程B)可能進入 if
,分配單例實例的內存,而後退出。
當系統上下文切換回線程A,你會分配另一個單例實例的內存,而後退出。在那個時間點,你有了兩個單例的實例——很明顯這不是你想要的(譯者注:這還能叫單例嗎?)!
要強制這個(競態)條件發生,替換 PhotoManager.m
中的 sharedManager
爲下面的實現:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
上面的代碼中你用 NSThread 的 sleepForTimeInterval:
類方法來強制發生一個上下文切換。
打開 AppDelegate.m
並添加以下代碼到 application:didFinishLaunchingWithOptions:
的最開始處:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; });
這裏建立了多個異步併發調用來實例化單例,而後引起上面描述的競態條件。
編譯並運行項目;查看控制檯輸出,你會看到多個單例被實例化,以下所示:
注意到這裏有好幾行顯示着不一樣地址的單例實例。這明顯違背了單例的目的,對吧?:]
這個輸出向你展現了臨界區被執行屢次,而它只應該執行一次。如今,當然是你本身強制這樣的情況發生,但你能夠想像一下這個情況會怎樣在無心間發生。
注意:基於其它你沒法控制的系統事件,NSLog 的數量有時會顯示多個。線程問題極其難以調試,由於它們每每難以重現。
要糾正這個情況,實例化代碼應該只執行一次,並阻塞其它實例在 if
條件的臨界區運行。這恰好就是 dispatch_once
能作的事。
在單例初始化方法中用 dispatch_once
取代 if
條件判斷,以下所示:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
編譯並運行你的應用;查看控制檯輸出,你會看到有且僅有一個單例的實例——這就是你對單例的指望!:]
如今你已經明白了防止競態條件的重要性,從 AppDelegate.m
中移除 dispatch_async
語句,並用下面的實現替換 PhotoManager
單例的初始化:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
dispatch_once()
以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once
的代碼)的不一樣的線程會在臨界區已有一個線程的狀況下被阻塞,直到臨界區完成爲止。
須要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類自己線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的狀況。這些須要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。
線程安全實例不是處理單例時的惟一問題。若是單例屬性表示一個可變對象,那麼你就須要考慮是否那個對象自身線程安全。
若是問題中的這個對象是一個 Foundation 容器類,那麼答案是——「極可能不安全」!Apple 維護一個有用且有些心寒的列表,衆多的 Foundation 類都不是線程安全的。 NSMutableArray
,已用於你的單例,正在那個列表裏休息。
雖然許多線程能夠同時讀取 NSMutableArray
的一個實例而不會產生問題,但當一個線程正在讀取時讓另一個線程修改數組就是不安全的。你的單例在目前的情況下不能預防這種狀況的發生。
要分析這個問題,看看 PhotoManager.m
中的 addPhoto:
,轉載以下:
- (void)addPhoto:(Photo *)photo
{
if (photo) { [_photosArray addObject:photo]; dispatch_async(dispatch_get_main_queue(), ^{ [self postContentAddedNotification]; }); } }
這是一個寫
方法,它修改一個私有可變數組對象。
如今看看 photos
,轉載以下:
- (NSArray *)photos
{
return [NSArray arrayWithArray:_photosArray]; }
這是所謂的讀
方法,它讀取可變數組。它爲調用者生成一個不可變的拷貝,防止調用者不當地改變數組,但這不能提供任何保護來對抗當一個線程調用讀方法 photos
的同時另外一個線程調用寫方法 addPhoto:
。
這就是軟件開發中經典的讀者寫者問題
。GCD 經過用 dispatch barriers
建立一個讀者寫者鎖
提供了一個優雅的解決方案。
Dispatch barriers 是一組函數,在併發隊列上工做時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上惟一被執行的條目。這就意味着全部的先於調度障礙提交到隊列的條目必能在這個 Block 執行前完成。
當這個 Block 的時機到達,調度障礙執行這個 Block 並確保在那個時間裏隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。
下圖顯示了障礙函數對多個異步隊列的影響:
注意到正常部分的操做就如同一個正常的併發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是惟一在執行的事物。在障礙完成後,隊列回到一個正常併發隊列的樣子。
下面是你什麼時候會——和不會——使用障礙函數的狀況:
因爲上面惟一像樣的選擇是自定義併發隊列,你將建立一個你本身的隊列去處理你的障礙函數並分開讀和寫函數。且這個併發隊列將容許多個多操做同時進行。
打開 PhotoManager.m
,添加以下私有屬性到類擴展中:
@interface PhotoManager () @property (nonatomic,strong,readonly) NSMutableArray *photosArray; @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this @end
找到 addPhoto:
並用下面的實現替換它:
- (void)addPhoto:(Photo *)photo
{
if (photo) { // 1 dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2 [_photosArray addObject:photo]; // 3 dispatch_async(dispatch_get_main_queue(), ^{ // 4 [self postContentAddedNotification]; }); }); } }
你新寫的函數是這樣工做的:
這就處理了寫操做,但你還須要實現 photos
讀方法並實例化 concurrentPhotoQueue
。
在寫者打擾的狀況下,要確保線程安全,你須要在 concurrentPhotoQueue
隊列上執行讀操做。既然你須要從函數返回,你就不能異步調度到隊列,由於那樣在讀者函數返回以前不必定運行。
在這種狀況下,dispatch_sync
就是一個絕好的候選。
dispatch_sync()
同步地提交工做並在返回前等待它完成。使用 dispatch_sync
跟蹤你的調度障礙工做,或者當你須要等待操做完成後才能使用 Block 處理過的數據。若是你使用第二種狀況作事,你將不時看到一個 __block
變量寫在 dispatch_sync
範圍以外,以便返回時在 dispatch_sync
使用處理過的對象。
但你須要很當心。想像若是你調用 dispatch_sync
並放在你已運行着的當前隊列。這會致使死鎖,由於調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務沒法完成!這將迫使你自覺於你正從哪一個隊列調用——以及你正在傳遞進入哪一個隊列。
下面是一個快速總覽,關於在什麼時候以及何處使用 dispatch_sync
:
dispatch_sync
放在同一個隊列,那你就百分百地建立了一個死鎖。繼續在 PhotoManager.m
上工做,用下面的實現替換 photos
:
- (NSArray *)photos
{
__block NSArray *array; // 1 dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 array = [NSArray arrayWithArray:_photosArray]; // 3 }); return array; }
這就是你的讀函數。按順序看看編過號的註釋,有這些:
__block
關鍵字容許對象在 Block 內可變。沒有它,array
在 Block 內部就只是只讀的,你的代碼甚至不能經過編譯。concurrentPhotoQueue
上同步調度來執行讀操做。array
內並返回它。最後,你須要實例化你的 concurrentPhotoQueue
屬性。修改 sharedManager
以便像下面這樣初始化隊列:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; // ADD THIS: sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT); }); return sharedPhotoManager; }
這裏使用 dispatch_queue_create
初始化 concurrentPhotoQueue
爲一個併發隊列。第一個參數是反向DNS樣式命名慣例;確保它是描述性的,將有助於調試。第二個參數指定你的隊列是串行仍是併發。
注意:當你在網上搜索例子時,你會常常看人們傳遞
0
或者NULL
給dispatch_queue_create
的第二個參數。這是一個建立串行隊列的過期方式;明確你的參數老是更好。
恭喜——你的 PhotoManager
單例如今是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。
依然沒有 100% 地掌握 GCD 的要領?確保你可使用 GCD 函數輕鬆地建立簡單的例子,使用斷點和 NSLog
語句保證本身明白當下發生的狀況。
我在下面提供了兩個 GIF動畫來幫助你鞏固對 dispatch_async
和 dispatch_sync
的理解。包含在每一個 GIF 中的代碼能夠提供視覺輔助;仔細注意 GIF 左邊顯示代碼斷點的每一步,以及右邊相關隊列的狀態。
- (void)viewDidLoad
{
[super viewDidLoad]; dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }
下面是圖中幾個步驟的說明:
UIViewController
的任務,其中包含了 viewDidLoad
。viewDidLoad
在主線程執行。viewDidLoad
內,正要到達 dispatch_sync
。dispatch_sync
Block 被添加到一個全局隊列中,將在稍後執行。進程將在主線程掛起直到該 Block 完成。同時,全局隊列併發處理任務;要記得 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。dispatch_sync
Block 加入以前已經出如今隊列中的任務。dispatch_sync
Block 。viewDidLoad
方法完成,主隊列繼續處理其餘任務。dispatch_sync
添加任務到一個隊列並等待直到任務完成。dispatch_async
作相似的事情,但不一樣之處是它不會等待任務的完成,而是當即繼續「調用線程」的其它任務。
- (void)viewDidLoad
{
[super viewDidLoad]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"First Log"); }); NSLog(@"Second Log"); }
UIViewController
的任務,其中包含了 viewDidLoad
。viewDidLoad
在主線程執行。viewDidLoad
內,正要到達 dispatch_async
。dispatch_async
Block 被添加到一個全局隊列中,將在稍後執行。viewDidLoad
在添加 dispatch_async
到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列併發地處理它未完成地任務。記住 Block 在全局隊列中將按照 FIFO 順序出列,但能夠併發執行。dispatch_async
的代碼塊開始執行。dispatch_async
Block 完成,兩個 NSLog
語句將它們的輸出放在控制檯上。在這個特定的實例中,第二個 NSLog
語句執行,跟着是第一個 NSLog
語句。並不老是這樣——着取決於給定時刻硬件正在作的事情,並且你沒法控制或知曉哪一個語句會先執行。「第一個」 NSLog
在某些調用狀況下會第一個執行。
在本教程中,你學習瞭如何讓你的代碼線程安全,以及在執行 CPU 密集型任務時如何保持主線程的響應性。
你能夠下載 GooglyPuff 項目,它包含了目前全部本教程中編寫的實現。在本教程的第二部分,你將繼續改進這個項目。
若是你計劃優化你本身的應用,那你應該用 Instruments
中的 Time Profile
模版分析你的工做。對這個工具的使用超出了本教程的範圍,你能夠看看 如何使用Instruments 來獲得一個很好的概述。
同時請確保在真實設備上分析,而在模擬器上測試會對程序速度產生很是不許確的印象。
在教程的下一部分,你將更加深刻到 GCD 的 API 中,作一些更 Cool 的東西。
若是你有任何問題或評論,可自由地加入下方的討論!
二
歡迎來到GCD深刻理解系列教程的第二部分(也是最後一部分)。
在本系列的第一部分中,你已經學到超過你想像的關於併發、線程以及GCD 如何工做的知識。經過在初始化時利用 dispatch_once
,你建立了一個線程安全的 PhotoManager
單例,並且你經過使用 dispatch_barrier_async
和 dispatch_sync
的組合使得對 Photos
數組的讀取和寫入都變得線程安全了。
除了上面這些,你還經過利用 dispatch_after
來延遲顯示提示信息,以及利用 dispatch_async
將 CPU 密集型任務從 ViewController 的初始化過程當中剝離出來異步執行,達到了加強應用的用戶體驗的目的。
若是你一直跟着第一部分的教程在寫代碼,那你能夠繼續你的工程。但若是你沒有完成第一部分的工做,或者不想重用你的工程,你能夠下載第一部分最終的代碼。
那就讓咱們來更深刻地探索 GCD 吧!
你可能已經注意到當你嘗試用 Le Internet 選項來添加圖片時,一個 UIAlertView
會在圖片下載完成以前就彈出,以下如所示:
問題的癥結在 PhotoManagers 的 downloadPhotoWithCompletionBlock:
裏,它目前的實現以下:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error; for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } }]; [[PhotoManager sharedManager] addPhoto:photo]; } if (completionBlock) { completionBlock(error); } }
在方法的最後你調用了 completionBlock
——由於此時你假設全部的照片都已下載完成。但很不幸,此時並不能保證全部的下載都已完成。
Photo
類的實例方法用某個 URL 開始下載某個文件並當即返回,但此時下載並未完成。換句話說,當 downloadPhotoWithCompletionBlock:
在其末尾調用 completionBlock
時,它就假設了它本身所使用的方法全都是同步的,並且每一個方法都完成了它們的工做。
然而,-[Photo initWithURL:withCompletionBlock:]
是異步執行的,會當即返回——因此這種方式行不通。
所以,只有在全部的圖像下載任務都調用了它們本身的 Completion Block 以後,downloadPhotoWithCompletionBlock:
才能調用它本身的 completionBlock
。問題是:你該如何監控併發的異步事件?你不知道它們什麼時候完成,並且它們完成的順序徹底是不肯定的。
或許你能夠寫一些比較 Hacky 的代碼,用多個布爾值來記錄每一個下載的完成狀況,但這樣作就缺失了擴展性,並且說實話,代碼會很難看。
幸運的是, 解決這種對多個異步任務的完成進行監控的問題,剛好就是設計 dispatch_group 的目的。
Dispatch Group 會在整個組的任務都完成時通知你。這些任務能夠是同步的,也能夠是異步的,即使在不一樣的隊列也行。並且在整個組的任務都完成時,Dispatch Group 能夠用同步的或者異步的方式通知你。由於要監控的任務在不一樣隊列,那就用一個 dispatch_group_t
的實例來記下這些不一樣的任務。
當組中全部的事件都完成時,GCD 的 API 提供了兩種通知方式。
第一種是 dispatch_group_wait
,它會阻塞當前線程,直到組裏面全部的任務都完成或者等到某個超時發生。這剛好是你目前所須要的。
打開 PhotoManager.m,用下列實現替換 downloadPhotosWithCompletionBlock:
:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); // 2 for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 3 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 4 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5 dispatch_async(dispatch_get_main_queue(), ^{ // 6 if (completionBlock) { // 7 completionBlock(error); } }); }); }
按照註釋的順序,你會看到:
dispatch_group_wait
,它會阻塞當前線程,因此你要用 dispatch_async
將整個方法放入後臺隊列以免阻塞主線程。dispatch_group_enter
手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter
和 dispatch_group_leave
成對出現,不然你可能會遇到詭異的崩潰問題。dispatch_group_wait
會一直等待,直到任務所有完成或者超時。若是在全部任務完成前超時了,該函數會返回一個非零值。你能夠對此返回值作條件判斷以肯定是否超出等待週期;然而,你在這裏用 DISPATCH_TIME_FOREVER
讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,由於圖片的建立工做老是會完成的。completionBlock
回調。這會將工做放到主線程上,並在稍後執行。completionBlock
是否爲 nil,若是不是,那就運行它。編譯並運行你的應用,嘗試下載多個圖片,觀察你的應用是在什麼時候運行 completionBlock 的。
注意:若是你是在真機上運行應用,並且網絡活動發生得太快以至難以觀察 completionBlock 被調用的時刻,那麼你能夠在 Settings 應用裏的開發者相關部分裏打開一些網絡設置,以確保代碼按照咱們所指望的那樣工做。只需去往 Network Link Conditioner 區,開啓它,再選擇一個 Profile,「Very Bad Network」 就不錯。
若是你是在模擬器裏運行應用,你可使用 來自 GitHub 的 Network Link Conditioner 來改變網絡速度。它會成爲你工具箱中的一個好工具,由於它強制你研究你的應用在鏈接速度並不是最佳的狀況下會變成什麼樣。
目前爲止的解決方案還不錯,可是整體來講,若是可能,最好仍是要避免阻塞線程。你的下一個任務是重寫一些方法,以便當全部下載任務完成時能異步通知你。
在咱們轉向另一種使用 Dispatch Group 的方式以前,先看一個簡要的概述,關於什麼時候以及怎樣使用有着不一樣的隊列類型的 Dispatch Group :
上面的一切都很好,但在另外一個隊列上異步調度而後使用 dispatch_group_wait 來阻塞實在顯得有些笨拙。是的,還有另外一種方式……
在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock:
方法,用下面的實現替換它:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
// 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 2 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 3 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4 if (completionBlock) { completionBlock(error); } }); }
下面解釋新的異步方法如何工做:
async
調用中。enter
方法,沒作任何修改。leave
方法,也沒作任何修改。dispatch_group_notify
以異步的方式工做。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那麼 completionBlock
便會運行。你還指定了運行 completionBlock
的隊列,此處,主隊列就是你所須要的。對於這個特定的工做,上面的處理明顯更清晰,並且也不會阻塞任何線程。
既然你的工具箱裏有了這些新工具,你大概作任何事情都想使用它們,對吧?
看看 PhotoManager 中的 downloadPhotosWithCompletionBlock
方法。你可能已經注意到這裏的 for
循環,它迭代三次,下載三個不一樣的圖片。你的任務是嘗試讓 for
循環併發運行,以提升其速度。
dispatch_apply
恰好可用於這個任務。
dispatch_apply
表現得就像一個 for
循環,但它能併發地執行不一樣的迭代。這個函數是同步的,因此和普通的 for
循環同樣,它只會在全部工做都完成後纔會返回。
當在 Block 內計算任何給定數量的工做的最佳迭代數量時,必需要當心,由於過多的迭代和每一個迭代只有少許的工做會致使大量開銷以至它能抵消任何因併發帶來的收益。而被稱爲跨越式(striding)
的技術能夠在此幫到你,即經過在每一個迭代裏多作幾個不一樣的工做。
譯者注:大概就能減小併發數量吧,做者是提醒你們注意併發的開銷,記在內心!
那什麼時候才適合用 dispatch_apply
呢?
dispatch_apply
的功能;你還不如直接使用普通的 for
循環。dispatch_apply
。仍是用普通的 for
循環吧。回到 downloadPhotosWithCompletionBlock:
並用下列實現替換它:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
__block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); }]; [[PhotoManager sharedManager] addPhoto:photo]; }); dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ if (completionBlock) { completionBlock(error); } }); }
你的循環如今是並行運行的了;在上面的代碼中,在調用 dispatch_apply
時,你用第一次參數指明瞭迭代的次數,用第二個參數指定了任務運行的隊列,而第三個參數是一個 Block。
要知道雖然你有代碼保證添加相片時線程安全,但圖片的順序卻可能不一樣,這取決於線程完成的順序。
編譯並運行,而後從 「Le Internet」 添加一些照片。注意到區別了嗎?
在真機上運行新代碼會稍微更快的獲得結果。但咱們所作的這些提速工做真的值得嗎?
實際上,在這個例子裏並不值得。下面是緣由:
for
循環要多。若你要以合適的步長迭代很是大的集合,那才應該考慮使用 dispatch_apply
。記住,不要在優化上太瘋狂。你只會讓你本身和後來者更難以讀懂你的代碼。
等一下!還有更多!有一些額外的函數在不一樣的道路上走得更遠。雖然你不會太頻繁地使用這些工具,但在對的狀況下,它們能夠提供極大的幫助。
這可能聽起來像是個瘋狂的想法,但你知道 Xcode 已有了測試功能嗎?:] 我知道,雖然有時候我喜歡僞裝它不存在,但在代碼裏構建複雜關係時編寫和運行測試很是重要。
Xcode 裏的測試在 XCTestCase
的子類上執行,並運行任何方法簽名以 test
開頭的方法。測試在主線程運行,因此你能夠假設全部測試都是串行發生的。
當一個給定的測試方法運行完成,XCTest 方法將考慮此測試已結束,並進入下一個測試。這意味着任何來自前一個測試的異步代碼會在下一個測試運行時繼續運行。
網絡代碼一般是異步的,所以你不能在執行網絡獲取時阻塞主線程。也就是說,整個測試會在測試方法完成以後結束,這會讓對網絡代碼的測試變得很困難。也就是,除非你在測試方法內部阻塞主線程直到網絡代碼完成。
注意:有一些人會說,這種類型的測試不屬於集成測試的首選集(Preferred Set)。一些人會贊同,一些人不會。但若是你想作,那就去作。
導航到 GooglyPuffTests.m 並查看 downloadImageURLWithString:
,以下:
- (void)downloadImageURLWithString:(NSString *)URLString { NSURL *url = [NSURL URLWithString:URLString]; __block BOOL isFinishedDownloading = NO; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } isFinishedDownloading = YES; }]; while (!isFinishedDownloading) {} }
這是一種測試異步網絡代碼的幼稚方式。 While 循環在函數的最後一直等待,直到 isFinishedDownloading
布爾值變成 True,它只會在 Completion Block 裏發生。讓咱們看看這樣作有什麼影響。
經過在 Xcode 中點擊 Product / Test 運行你的測試,若是你使用默認的鍵綁定,也可使用快捷鍵 ⌘+U 來運行你的測試。
在測試運行時,注意 Xcode debug 導航欄裏的 CPU 使用率。這個設計不當的實現就是一個基本的 自旋鎖 。它很不實用,由於你在 While 循環裏浪費了珍貴的 CPU 週期;並且它也幾乎沒有擴展性。
譯者注:所謂自旋鎖,就是某個線程一直搶佔着 CPU 不斷檢查以等到它須要的狀況出現。由於現代操做系統都是能夠併發運行多個線程的,因此它所等待的那個線程也有機會被調度執行,這樣它所須要的狀況遲早會出現。
你可能須要使用前面提到的 Network Link Conditioner ,已便清楚地看到這個問題。若是你的網絡太快,那麼自旋只會在很短的時間裏發生,難以觀察。
譯者注:做者反覆提到網速太快,而咱們還須要對付 GFW,簡直淚流滿面!
你須要一個更優雅、可擴展的解決方案來阻塞線程直到資源可用。歡迎來到信號量。
信號量是一種老式的線程概念,由很是謙卑的 Edsger W. Dijkstra 介紹給世界。信號量之因此比較複雜是由於它創建在操做系統的複雜性之上。
若是你想學到更多關於信號量的知識,看看這個連接它更細緻地討論了信號量理論。若是你是學術型,那能夠看一個軟件開發中經典的哲學家進餐問題,它須要使用信號量來解決。
信號量讓你控制多個消費者對有限數量資源的訪問。舉例來講,若是你建立了一個有着兩個資源的信號量,那同時最多隻能有兩個線程能夠訪問臨界區。其餘想使用資源的線程必須在一個…你猜到了嗎?…FIFO隊列裏等待。
讓咱們來使用信號量吧!
打開 GooglyPuffTests.m 並用下列實現替換 downloadImageURLWithString:
:
- (void)downloadImageURLWithString:(NSString *)URLString { // 1 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSURL *url = [NSURL URLWithString:URLString]; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } // 2 dispatch_semaphore_signal(semaphore); }]; // 3 dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds); if (dispatch_semaphore_wait(semaphore, timeoutTime)) { XCTFail(@"%@ timed out", URLString); } }
下面來講明你代碼中的信號量是如何工做的:
再次運行測試。只要你有一個正常工做的網絡鏈接,這個測試就會立刻成功。請特別注意 CPU 的使用率,與以前使用自旋鎖的實現做個對比。
關閉你的網絡連接再運行測試;若是你在真機上運行,就打開飛行模式。若是你的在模擬器裏運行,你能夠直接斷開 Mac 的網絡連接。測試會在 10 秒後失敗。這很棒,它真的能按照預想的那樣工做!
還有一些瑣碎的測試,但若是你與一個服務器組協同工做,那麼這些基本的測試可以防止其餘人就最新的網絡問題對你說三道四。
GCD 的一個特別有趣的特性是 Dispatch Source,它基本上就是一個低級函數的 grab-bag ,能幫助你去響應或監測 Unix 信號、文件描述符、Mach 端口、VFS 節點,以及其它晦澀的東西。全部這些都超出了本教程討論的範圍,但你能夠經過實現一個 Dispatch Source 對象並以一個至關奇特的方式來使用它來品嚐那些晦澀的東西。
第一次使用 Dispatch Source 可能會迷失在如何使用一個源,因此你須要知曉的第一件事是 dispatch_source_create
如何工做。下面是建立一個源的函數原型:
dispatch_source_t dispatch_source_create( dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue);
第一個參數是 dispatch_source_type_t
。這是最重要的參數,由於它決定了 handle 和 mask 參數將會是什麼。你能夠查看 Xcode 文檔 獲得哪些選項可用於每一個 dispatch_source_type_t
參數。
下面你將監控 DISPATCH_SOURCE_TYPE_SIGNAL
。如文檔所顯示的:
一個監控當前進程信號的 Dispatch Source。 handle 是信號編號,mask 未使用(傳 0 便可)。
這些 Unix 信號組成的列表可在頭文件 signal.h 中找到。在其頂部有一堆 #define
語句。你將監控此信號列表中的 SIGSTOP
信號。這個信號將會在進程接收到一個沒法迴避的暫停指令時被髮出。在你用 LLDB 調試器調試應用時你使用的也是這個信號。
去往 PhotoCollectionViewController.m 並添加以下代碼到 viewDidLoad
的頂部,就在 [super viewDidLoad]
下面:
- (void)viewDidLoad
{
[super viewDidLoad]; // 1 #if DEBUG // 2 dispatch_queue_t queue = dispatch_get_main_queue(); // 3 static dispatch_source_t source = nil; // 4 __typeof(self) __weak weakSelf = self; // 5 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 6 source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue); // 7 if (source) { // 8 dispatch_source_set_event_handler(source, ^{ // 9 NSLog(@"Hi, I am: %@", weakSelf); }); dispatch_resume(source); // 10 } }); #endif // The other stuff }
這些代碼有點兒複雜,因此跟着註釋一步步走,看看到底發生了什麼:
dispatch_queue_t
實例變量而不是在參數上直接使用函數。當代碼變長,分拆有助於可讀性。source
在方法範圍以外也可被訪問,因此你使用了一個 static 變量。weakSelf
以確保不會出現保留環(Retain Cycle)。這對 PhotoCollectionViewController
來講不是徹底必要的,由於它會在應用的整個生命期裏保持活躍。然而,若是你有任何其它會消失的類,這就能確保不會出現保留環而形成內存泄漏。dispatch_once
確保只會執行一次 Dispatch Source 的設置。source
變量。你指明瞭你對信號監控感興趣並提供了 SIGSTOP
信號做爲第二個參數。進一步,你使用主隊列處理接收到的事件——很快你就好發現爲什麼要這樣作。dispatch_source_set_event_handler
就會執行。以後你能夠在其 Block 裏設置合適的邏輯處理器(Logic Handler)。NSLog
語句,它將對象打印到控制檯。編譯並運行應用;在調試器裏暫停並當即恢復應用,查看控制檯,你會看到這個來自黑暗藝術的函數確實能夠工做。你看到的大概以下:
2014-03-29 17:41:30.610 GooglyPuff[8181:60b] Hi, I am:
你的應用如今具備調試感知了!這真是超級棒,但在真實世界裏該如何使用它呢?
你能夠用它去調試一個對象並在任何你想恢復應用的時候顯示數據;你一樣能給你的應用加上自定義的安全邏輯以便在惡意攻擊者將一個調試器鏈接到你的應用上時保護它本身(或用戶的數據)。
譯者注:好像挺有用!
一個有趣的主意是,使用此方式的做爲一個堆棧追蹤工具去找到你想在調試器裏操縱的對象。
稍微想一想這個狀況。當你意外地中止調試器,你幾乎歷來都不會在所需的棧幀上。如今你能夠在任什麼時候候中止調試器並在你所需的地方執行代碼。若是你想在你的應用的某一點執行的代碼很是難以從調試器訪問的話,這會很是有用。有機會試試吧!
將一個斷點放在你剛添加在 viewDidLoad 裏的事件處理器的 NSLog
語句上。在調試器裏暫停,而後再次開始;應用會到達你添加的斷點。如今你深刻到你的 PhotoCollectionViewController 方法深處。你能夠訪問 PhotoCollectionViewController 的實例獲得你關心的內容。很是方便!
注意:若是你尚未注意到在調試器裏的是哪一個線程,那如今就看看它們。主線程老是第一個被 libdispatch 跟隨,它是 GCD 的座標,做爲第二個線程。以後,線程計數和剩餘線程取決於硬件在應用到達斷點時正在作的事情。
在調試器裏,鍵入命令:po [[weakSelf navigationItem] setPrompt:@"WOOT!"]
而後恢復應用的執行。你會看到以下內容:
使用這個方法,你能夠更新 UI、查詢類的屬性,甚至是執行方法——全部這一切都不須要重啓應用併到達某個特定的工做狀態。至關優美吧!
譯者注:發揮這一點,是能夠作出一些調試庫的吧?
你能夠在此下載最終的項目。
我討厭再次說起此主題,但你真的要看看 如何使用 Instruments 教程。若是你計劃優化你的應用,那你必定要學會使用它。請注意 Instruments 擅長於分析相對執行:比較哪些區域的代碼相對於其它區域的代碼花費了更長的時間。若是你嘗試計算出某個方法實際的執行時間,那你可能須要拿出更多的自釀的解決方案(Home-brewed Solution)。
一樣請看看 如何使用 NSOperations 和 NSOperationQueues 吧,它們是創建在 GCD 之上的併發技術。大致來講,若是你在寫簡單的用過就忘的任務,那它們就是使用 GCD 的最佳實踐,。NSOperations 提供更好的控制、處理大量併發操做的實現,以及一個以速度爲代價的更加面向對象的範例。
記住,除非你有特別的緣由要往下流走(譯者的玩笑:即便用低級別 API),不然永遠應嘗試並堅持使用高級的 API。若是你想學到更多或想作某些很是很是「有趣」的事情,那你就應該冒險進入 Apple 的黑暗藝術。
祝你好運,玩得開心!有任何問題或反饋請在下方的討論區貼出!