天天看點

IOS開發—深入了解 GCD(二)

原創位址:http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2

歡迎來到GCD深入了解系列教程的第二部分(也是最後一部分)。

在本系列的第一部分中,你已經學到超過你想像的關于并發、線程以及GCD 如何工作的知識。通過在初始化時利用

dispatch_once

,你建立了一個線程安全的 

PhotoManager

 單例,而且你通過使用 

dispatch_barrier_async

 和

dispatch_sync

 的組合使得對 

Photos

 數組的讀取和寫入都變得線程安全了。

除了上面這些,你還通過利用 

dispatch_after

 來延遲顯示提示資訊,以及利用 

dispatch_async

 将 CPU 密集型任務從 ViewController 的初始化過程中剝離出來異步執行,達到了增強應用的使用者體驗的目的。

如果你一直跟着第一部分的教程在寫代碼,那你可以繼續你的工程。但如果你沒有完成第一部分的工作,或者不想重用你的工程,你可以下載下傳第一部分最終的代碼。

那就讓我們來更深入地探索 GCD 吧!

糾正過早彈出的提示

你可能已經注意到當你嘗試用 Le Internet 選項來添加圖檔時,一個 

UIAlertView

 會在圖檔下載下傳完成之前就彈出,如下如所示:

IOS開發—深入了解 GCD(二)

問題的症結在 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);
            }
        });
    });
}
           

按照注釋的順序,你會看到:

  1. 因為你在使用的是同步的 

    dispatch_group_wait

     ,它會阻塞目前線程,是以你要用 

    dispatch_async

     将整個方法放入背景隊列以避免阻塞主線程。
  2. 建立一個新的 Dispatch Group,它的作用就像一個用于未完成任務的計數器。
  3. dispatch_group_enter

     手動通知 Dispatch Group 任務已經開始。你必須保證 

    dispatch_group_enter

     和

    dispatch_group_leave

     成對出現,否則你可能會遇到詭異的崩潰問題。
  4. 手動通知 Group 它的工作已經完成。再次說明,你必須要確定進入 Group 的次數和離開 Group 的次數相等。
  5. dispatch_group_wait

     會一直等待,直到任務全部完成或者逾時。如果在所有任務完成前逾時了,該函數會傳回一個非零值。你可以對此傳回值做條件判斷以确定是否超出等待周期;然而,你在這裡用 

    DISPATCH_TIME_FOREVER

     讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,因為圖檔的建立工作總是會完成的。
  6. 此時此刻,你已經確定了,要麼所有的圖檔任務都已完成,要麼發生了逾時。然後,你在主線程上運作

    completionBlock

     回調。這會将工作放到主線程上,并在稍後執行。
  7. 最後,檢查 

    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);
        }
    });
}
           

下面解釋新的異步方法如何工作:

  1. 在新的實作裡,因為你沒有阻塞主線程,是以你并不需要将方法包裹在 

    async

     調用中。
  2. 同樣的 

    enter

     方法,沒做任何修改。
  3. 同樣的 

    leave

     方法,也沒做任何修改。
  4. dispatch_group_notify

     以異步的方式工作。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那麼

    completionBlock

     便會運作。你還指定了運作 

    completionBlock

     的隊列,此處,主隊列就是你所需要的。

對于這個特定的工作,上面的處理明顯更清晰,而且也不會阻塞任何線程。

太多并發帶來的風險

既然你的工具箱裡有了這些新工具,你大概做任何事情都想使用它們,對吧?

IOS開發—深入了解 GCD(二)

看看 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)。一些人會贊同,一些人不會。但如果你想做,那就去做。
IOS開發—深入了解 GCD(二)

導航到 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);
    }
}
           

下面來說明你代碼中的信号量是如何工作的:

  1. 建立一個信号量。參數指定信号量的起始值。這個數字是你可以通路的信号量,不需要有人先去增加它的數量。(注意到增加信号量也被叫做發射信号量)。譯者注:這裡初始化為0,也就是說,有人想使用信号量必然會被阻塞,直到有人增加信号量。
  2. 在 Completion Block 裡你告訴信号量你不再需要資源了。這就會增加信号量的計數并告知其他想使用此資源的線程。
  3. 這會在逾時之前等待信号量。這個調用阻塞了目前線程直到信号量被發射。這個函數的一個非零傳回值表示到達逾時了。在這個例子裡,測試将會失敗因為它以為網絡請求不會超過 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
}
           

這些代碼有點兒複雜,是以跟着注釋一步步走,看看到底發生了什麼:

  1. 最好是在 DEBUG 模式下編譯這些代碼,因為這會給“有關方面(Interested Parties)”很多關于你應用的洞察。 :]
  2. Just to mix things up,你建立了一個 

    dispatch_queue_t

     執行個體變量而不是在參數上直接使用函數。當代碼變長,分拆有助于可讀性。
  3. 你需要 

    source

     在方法範圍之外也可被通路,是以你使用了一個 static 變量。
  4. 使用 

    weakSelf

     以確定不會出現保留環(Retain Cycle)。這對 

    PhotoCollectionViewController

     來說不是完全必要的,因為它會在應用的整個生命期裡保持活躍。然而,如果你有任何其它會消失的類,這就能確定不會出現保留環而造成記憶體洩漏。
  5. 使用 

    dispatch_once

     確定隻會執行一次 Dispatch Source 的設定。
  6. 初始化 

    source

     變量。你指明了你對信号監控感興趣并提供了 

    SIGSTOP

     信号作為第二個參數。進一步,你使用主隊列處理接收到的事件——很快你就好發現為何要這樣做。
  7. 如果你提供的參數不合格,那麼 Dispatch Source 對象不會被建立。也就是說,在你開始在其上工作之前,你需要確定已有了一個有效的 Dispatch Source 。
  8. 當你收到你所監控的信号時,

    dispatch_source_set_event_handler

     就會執行。之後你可以在其 Block 裡設定合适的邏輯處理器(Logic Handler)。
  9. 一個基本的 

    NSLog

     語句,它将對象列印到控制台。
  10. 預設的,所有源都初始為暫停狀态。如果你要開始監控事件,你必須告訴源對象恢複活躍狀态。

編譯并運作應用;在調試器裡暫停并立即恢複應用,檢視控制台,你會看到這個來自黑暗藝術的函數确實可以工作。你看到的大概如下:

1

2014-03-29 17:41:30.610 GooglyPuff[8181:60b] Hi, I am:

你的應用現在具有調試感覺了!這真是超級棒,但在真實世界裡該如何使用它呢?

你可以用它去調試一個對象并在任何你想恢複應用的時候顯示資料;你同樣能給你的應用加上自定義的安全邏輯以便在惡意攻擊者将一個調試器連接配接到你的應用上時保護它自己(或使用者的資料)。

譯者注:好像挺有用!

一個有趣的主意是,使用此方式的作為一個堆棧追蹤工具去找到你想在調試器裡操縱的對象。

IOS開發—深入了解 GCD(二)

稍微想想這個情況。當你意外地停止調試器,你幾乎從來都不會在所需的棧幀上。現在你可以在任何時候停止調試器并在你所需的地方執行代碼。如果你想在你的應用的某一點執行的代碼非常難以從調試器通路的話,這會非常有用。有機會試試吧!

IOS開發—深入了解 GCD(二)

将一個斷點放在你剛添加在 viewDidLoad 裡的事件處理器的 

NSLog

 語句上。在調試器裡暫停,然後再次開始;應用會到達你添加的斷點。現在你深入到你的 PhotoCollectionViewController 方法深處。你可以通路 PhotoCollectionViewController 的執行個體得到你關心的内容。非常友善!

注意:如果你還沒有注意到在調試器裡的是哪個線程,那現在就看看它們。主線程總是第一個被 libdispatch 跟随,它是 GCD 的坐标,作為第二個線程。之後,線程計數和剩餘線程取決于硬體在應用到達斷點時正在做的事情。

在調試器裡,鍵入指令:

po [[weakSelf navigationItem] setPrompt:@"WOOT!"]

然後恢複應用的執行。你會看到如下内容:

IOS開發—深入了解 GCD(二)
IOS開發—深入了解 GCD(二)

使用這個方法,你可以更新 UI、查詢類的屬性,甚至是執行方法——所有這一切都不需要重新開機應用并到達某個特定的工作狀态。相當優美吧!

譯者注:發揮這一點,是可以做出一些調試庫的吧?