歡迎來到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 Groups(調度組)
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,它的作用就像一個用於未完成任務的計數器。
dispatch_group_enter
手動通知 Dispatch Group 任務已經開始。你必須保證dispatch_group_enter
和dispatch_group_leave
成對出現,否則你可能會遇到詭異的崩潰問題。- 手動通知 Group 它的工作已經完成。再次說明,你必須要確保進入 Group 的次數和離開 Group 的次數相等。
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 :
- 自定義串行隊列:它很適合當一組任務完成時發出通知。
- 主隊列(串行):它也很適合這樣的情況。但如果你要同步地等待所有工作地完成,那你就不應該使用它,因爲你不能阻塞主線程。然而,異步模型是一個很有吸引力的能用於在幾個較長任務(例如網絡調用)完成後更新 UI 的方式。
- 併發隊列:它也很適合 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
。 - 你用於創建應用的時間是有限的——除非實在太糟糕否則不要浪費時間去提前優化代碼。如果你要優化什麼,那去優化那些明顯值得你付出時間的部分。你可以通過在 Instruments 裏分析你的應用,找出最長運行時間的方法。看看 如何在 Xcode 中使用 Instruments 可以學到更多相關知識。
- 通常情況下,優化代碼會讓你的代碼更加複雜,不利於你自己和其他開發者閱讀。請確保添加的複雜性能換來足夠多的好處。
記住,不要在優化上太瘋狂。你只會讓你自己和後來者更難以讀懂你的代碼。
GCD 的其他趣味
等一下!還有更多!有一些額外的函數在不同的道路上走得更遠。雖然你不會太頻繁地使用這些工具,但在對的情況下,它們可以提供極大的幫助。
阻塞——正確的方式
這可能聽起來像是個瘋狂的想法,但你知道 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);
}
}
下面來說明你代碼中的信號量是如何工作的:
- 創建一個信號量。參數指定信號量的起始值。這個數字是你可以訪問的信號量,不需要有人先去增加它的數量。(注意到增加信號量也被叫做發射信號量)。譯者注:這裏初始化爲0,也就是說,有人想使用信號量必然會被阻塞,直到有人增加信號量。
- 在 Completion Block 裏你告訴信號量你不再需要資源了。這就會增加信號量的計數並告知其他想使用此資源的線程。
- 這會在超時之前等待信號量。這個調用阻塞了當前線程直到信號量被髮射。這個函數的一個非零返回值表示到達超時了。在這個例子裏,測試將會失敗因爲它以爲網絡請求不會超過 10 秒鐘就會返回——一個平衡點!
再次運行測試。只要你有一個正常工作的網絡連接,這個測試就會馬上成功。請特別注意 CPU 的使用率,與之前使用自旋鎖的實現作個對比。
關閉你的網絡鏈接再運行測試;如果你在真機上運行,就打開飛行模式。如果你的在模擬器裏運行,你可以直接斷開 Mac 的網絡鏈接。測試會在 10 秒後失敗。這很棒,它真的能按照預想的那樣工作!
還有一些瑣碎的測試,但如果你與一個服務器組協同工作,那麼這些基本的測試能夠防止其他人就最新的網絡問題對你說三道四。
使用 Dispatch Source
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
}
這些代碼有點兒複雜,所以跟着註釋一步步走,看看到底發生了什麼:
- 最好是在 DEBUG 模式下編譯這些代碼,因爲這會給“有關方面(Interested Parties)”很多關於你應用的洞察。 :]
- Just to mix things up,你創建了一個
dispatch_queue_t
實例變量而不是在參數上直接使用函數。當代碼變長,分拆有助於可讀性。 - 你需要
source
在方法範圍之外也可被訪問,所以你使用了一個 static 變量。 - 使用
weakSelf
以確保不會出現保留環(Retain Cycle)。這對PhotoCollectionViewController
來說不是完全必要的,因爲它會在應用的整個生命期裏保持活躍。然而,如果你有任何其它會消失的類,這就能確保不會出現保留環而造成內存泄漏。 - 使用
dispatch_once
確保只會執行一次 Dispatch Source 的設置。 - 初始化
source
變量。你指明瞭你對信號監控感興趣並提供了SIGSTOP
信號作爲第二個參數。進一步,你使用主隊列處理接收到的事件——很快你就好發現爲何要這樣做。 - 如果你提供的參數不合格,那麼 Dispatch Source 對象不會被創建。也就是說,在你開始在其上工作之前,你需要確保已有了一個有效的 Dispatch Source 。
- 當你收到你所監控的信號時,
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 的黑暗藝術。