Parse源碼淺析系列(一)---Parse的底層多線程處理思路:GCD高級用法


Parse源碼淺析系列(一)---Parse的底層多線程處理思路:GCD高級用法

【前言】從iOS7升到iOS8後,GCD 出現了一個重大的變化:在 iOS7 時,使用 GCD 的並行隊列, dispatch_async 最大開啓的線程一直能控制在6、7條,線程數都是個位數,然而 iOS8後,最大線程數一度可以達到40條、50條。然而在文檔上並沒有對這一做法的目的進行介紹。

筆者推測 Apple 的目的是想借此讓開發者使用 NSOperationQueue :GCD 中 Apple 並沒有提供控制併發數量的接口,而NSOperationQueue 有。GCD 沒有提供暫停、恢復、取消隊列任務的接口,而 NSOperationQueue 有,如果想讓 GCD 支持 NSOperationQueue 原生就支持的功能,需要使用許多GCD 的高級功能,大大提高了使用的難度。

Apple 始終有一個觀念:儘可能選用高層 API,只在確有必要時才求助於底層。然而開發者並不買賬,在我進行的一次調查 中發現了一個有趣的現象:

大概 80%的iOS 開發者會支持使用 GCD 來完成操作隊列的實現,而且有 60% 的開發已經在項目中使用。

enter image description here

更是有人這樣表態:

假如不讓他用 GCD:

enter image description here

這種現象一直存在,包括 ARC 與 MRC、SB建 UI 與純代碼建 UI、SQL 與 CoreData的爭論。

但是因爲是源碼解析的文章,而 Parse 的 SDK 沒有用一句的 NSOperation 的代碼,GCD 一路用到底,讓我也十分震驚。只能說明,寫 Parse 的這位開發者是藝高人膽大。而且既然 GCD 的支持者如此之多,那麼就談一談如何讓 GCD 能支持NSOperationQueue 原生就支持的功能。

今天雖然談了NSOperation原生功能的 GCD 版本實現,但並不代表我支持像 Parse 這樣 GCD 一路用到底。 業內一般的看法是這樣的:

GCD 雖然能夠實現暫停和終止,但開發還是靈活些好,那些 NSOperation 用起來方便的就直接用 NSOperation 的方式,不然蘋果多包那一層不是蛋疼,包括文章裏提到的 iOS8 後控制線程數的問題,不一定項目就一定要GCD一路到底。有時候需要支持一些高層級封裝功能比如: KVO 時 NSOperation 還是有它的優勢的。 GCD 反而是處理些比較簡單的操作或者是較系統級的比如:監視進程或者監視文件夾內文件的變化之類的比較合適。

(iOS開發學習交流羣:512437027)

第一篇的目的是通過解讀 Parse 源碼來展示GCD兩個高級用法: Dispatch Source (派發源)和 Dispatch Semaphore(信號量)。首先通過Parse 的“離線存儲對象”操作,來介紹 Dispatch Source (派發源);然後通過Parse 的單元測試中使用的技巧“強制把異步任務轉換爲同步任務來方便進行單元測試”來介紹Dispatch Semaphore (信號量)。我已將思路濃縮爲可運行的7個 Demo 中,詳見倉庫裏的 Demo1到 Demo7。

如果對 GCD 不太熟悉,請先讀下《GCD 掃盲篇》

  1. Dispatch Source分派源

    1. Parse-iOS-SDK介紹

    2. Parse 的“離線存儲對象”操作介紹

    3. Parse 的“離線存儲對象”實現介紹
    4. Dispatch Source 的使用步驟
      1. 第一步:創建一個Dispatch Source
      2. 第二步:創建Dispatch Source的事件處理方法
      3. 第三步:處理Dispatch Source的暫停與恢復操作
      4. 第四步:向Dispatch Source發送事件
    5. GCD真的不能像OperationQueue那樣終止任務?
    6. 完整例子Demo1:讓 Dispatch Source “幫” Dispatch Queue 實現暫停和恢復功能
    7. DispatchSource能通過合併事件的方式確保在高負載下正常工作
    8. Dispatch Source 與 Dispatch Queue 兩者在線程執行上的關係
    9. 讓 Dispatch Source 與 Dispatch Queue 同時實現暫停和恢復
    10. Parse “離線存儲對象”操作的代碼摘錄
  2. Dispatch Semaphore 信號量
    1. 在項目中的應用:強制把異步任務轉換爲同步任務來方便進行單元測試
    2. 使用Dispatch Semaphore控制併發線程數量

Parse-iOS-SDK介紹

《iOS開發週報:iOS 8.4.1 發佈,iOS 8 時代謝幕》 對 Facebook 旗下的 Parse有這樣一段介紹:

Parse-SDK-iOS-OSX:著名的 BaaS 公司 Parse 最近開源了它們的 iOS/OSX SDK。Parse 的服務雖然在國內可能訪問速度不是很理想,但是它們在服務的穩定性和 SDK 質量上一直有非常優異的表現。此次開源的 SDK 對於日常工作是 SDK 開發的開發者來說,是一個難得的學習機會。Parse 的存取操作涉及到很多多線程的問題,從 Parse SDK 的源代碼中可以看出,這個 SDK 的開發者對 iOS 開發多線程有着非常深厚的理解和功底,讓人歎服。我個人推薦對此感興趣的朋友可以嘗試從閱讀 internal 文件夾下的兩個EventuallyQueue 文件開始着手,研究下 Parse 的底層多線程處理思路。

類似的服務: Apple 的 Cloud​Kit 、 國內的 LeanCloud(原名 AVOS ) 。

Parse 的“離線存儲對象”操作介紹

大多數保存功能可以立刻執行,並通知應用“保存完畢”。不過若不需要知道保存完成的時間,則可使用“離線存儲對象”操作(saveEventually 或 deleteEventually) 來代替,也就是:

如果用戶目前尚未接入網絡,“離線存儲對象”操作(saveEventually 或 deleteEventually) 會緩存設備中的數據,並在網絡連接恢復後上傳。如果應用在網絡恢復之前就被關閉了,那麼當它下一次打開時,SDK 會自動再次嘗試保存操作。

所有 saveEventually(或 deleteEventually)的相關調用,將按照調用的順序依次執行。因此,多次對某一對象使用 saveEventually 是安全的。

國內的 LeanCloud(原名 AVOS ) 也提供了相同的功能,所以以上《Parse 的“離線存儲對象”操作介紹》部分完全摘錄自 LeanCloud 的文檔。詳見《LeanCloud官方文檔-iOS / OS X 數據存儲開發指南--離線存儲對象》

(利益相關聲明:本人目前就職於 LeanCloud(原名 AVOS ) )

Parse 的“離線存儲對象”實現介紹

Parse 的“離線存儲對象”操作(saveEventually 或 deleteEventually) 是通過 GCD 的 Dispatch Source (信號源)來實現的。下面對 Dispatch Source (信號源)進行一下介紹:

GCD中除了主要的 Dispatch Queue 外,還有不太引人注目的 Dispatch Source .它是BSD系內核慣有功能kqueue的包裝。kqueue 是在 XNU 內核中發生各種事件時,在應用程序編程方執行處理的技術。其 CPU 負荷非常小,儘量不佔用資源。kqueue 可以說是應用程序處理 XNU 內核中發生的各種事件的方法中最優秀的一種。

Dispatch Source 也使用在了 Core Foundation 框架的用於異步網絡的API CFSocket 中。因爲Foundation 框架的異步網絡 API 是通過CFSocket實現的,所以可享受到僅使用 Foundation 框架的 Dispatch Source 帶來的好處。

那麼優勢何在?使用的 Dispatch Source 而不使用 dispatch_async 的唯一原因就是利用聯結的優勢。

聯結的大致流程:在任一線程上調用它的的一個函數 dispatch_source_merge_data 後,會執行 Dispatch Source 事先定義好的句柄(可以把句柄簡單理解爲一個 block )。

這個過程叫 Custom event ,用戶事件。是 dispatch source 支持處理的一種事件。

簡單地說,這種事件是由你調用 dispatch_source_merge_data 函數來向自己發出的信號。

下面介紹下使用步驟:

Dispatch Source 的使用步驟

第一步:創建一個Dispatch Source

    // 詳見 Demo1、Demo2
    // 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。設定Main Dispatch Queue 爲追加處理的Dispatch Queue
    _processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
                                                    dispatch_get_main_queue());

下面對參數進行下解釋:

其中自定義源累積事件中傳遞過來的值,累積的方式可以是相加的,正如上面代碼中的DISPATCH_SOURCE_TYPE_DATA_ADD ,也可以是邏輯或 DISPATCH_SOURCE_TYPE_DATA_OR 。這是最常見的兩個 Dispatch Source 可以處理的事件。

Dispatch Source 可處理的所有事件。如下表所示:

名稱 內容
DISPATCH_SOURCE_TYPE_DATA_ADD 變量增加
DISPATCH_SOURCE_TYPE_DATA_OR 變量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口發送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 監測到與進程相關的事件
DISPATCH_SOURCE_TYPE_READ 可讀取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信號
DISPATCH_SOURCE_TYPE_TIMER 定時器
DISPATCH_SOURCE_TYPE_VNODE 文件系統有變更
DISPATCH_SOURCE_TYPE_WRITE 可寫入文件映像

自定義源也需要一個隊列,用來處理所有的響應句柄(block)。那麼豈不是有兩個隊列了?沒錯,至於 Dispatch Queue這個隊列的線程執行與 Dispatch Source這個隊列的線程執行的關係,下文會結合 Demo1和 Demo2進行詳細論述。

第二步:創建Dispatch Source的事件處理方法

分派源提供了高效的方式來處理事件。首先註冊事件處理程序,事件發生時會收到通知。如果在系統還沒有來得及通知你之前事件就發生了多次,那麼這些事件會被合併爲一個事件。這對於底層的高性能代碼很有用,但是OS應用開發者很少會用到這樣的功能。類似地,分派源可以響應UNIX信號、文件系統的變化、其他進程的變化以及Mach Port事件。它們中很多都在Mac系統上很有用,但是iOS開發者通常不會用到。

不過,自定義源在iOS中很有用,尤其是在性能至關重要的場合進行進度反饋。如下所示,首先創建一個源:自定義源累積事件中傳遞過來的值。累積方式可以是相加( DISPATCH_SOURCE_TYPE_DATA_ADD ), 也可以是邏輯或( DISPATCH_SOURCE_DATA_OR )。自定義源也需要一個隊列,用來處理所有的響應處理塊。

創建源後,需要提供相應的處理方法。當源生效時會分派註冊處理方法;當事件發生時會分派事件處理方法;當源被取消時會分派取消處理方法。自定義源通常只需要一個事件處理方法,可以像這樣創建:

 /*
  *省略部分: 
    指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。設定Main Dispatch Queue 爲追加處理的Dispatch Queue
    詳見Demo1、Demo2
  *
  */
    __block NSUInteger totalComplete = 0;
    dispatch_source_set_event_handler(_processingQueueSource, ^{
        //當處理事件被最終執行時,計算後的數據可以通過dispatch_source_get_data來獲取。這個數據的值在每次響應事件執行後會被重置,所以totalComplete的值是最終累積的值。
        NSUInteger value = dispatch_source_get_data(_processingQueueSource);
        totalComplete += value;
        NSLog(@"進度:%@", @((CGFloat)totalComplete/100));
    });

在同一時間,只有一個處理方法塊的實例被分派。如果這個處理方法還沒有執行完畢,另一個事件就發生了,事件會以指定方式(ADD或者OR)進行累積。通過合併事件的方式,系統即使在高負 載情況下也能正常工作。當處理事件件被最終執行時,計算後的數據可以通過 dispatch_source_get_data 來獲取。這個數據的值在每次響應事件執行後會被重置,所以上面例子中 totalComplete 的值是最終累積的值。

第三步:處理Dispatch Source的暫停與恢復操作

當追加大量處理到Dispatch Queue時,在追加處理的過程中,有時希望不執行已追加的處理。例如演算結果被Block截獲時,一些處理會對這個演算結果造成影響。

在這種情況下,只要掛起Dispatch Queue即可。當可以執行時再恢復。

dispatch_suspend(queue);

dispatch_resume 函數恢復指定的 Dispatch Queue . 這些函數對已經執行的處理沒有影響。掛起後,追加到 Dispatch Queue 中但尚未執行的處理在此之後停止執行。而恢復則使得這些處理能夠繼續執行。

分派源創建時默認處於暫停狀態,在分派源分派處理程序之前必須先恢復。因爲忘記恢復分派源的狀態而產生bug是常見的事兒。恢復的方法是調用 dispatch_resume :

dispatch_resume (source);

第四步:向Dispatch Source發送事件

恢復源後,就可以像下面的代碼片段這樣,通過 dispatch_source_merge_data 向分派源發送事件:

    //2.
    //恢復源後,就可以通過dispatch_source_merge_data向Dispatch Source(分派源)發送事件:
    //詳見Demo1、Demo2
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        for (NSUInteger index = 0; index < 100; index++) {
            dispatch_async(queue, ^{
            dispatch_source_merge_data(_processingQueueSource, 1);
            usleep(20000);//0.02秒
            });
        }

上面代碼在每次循環中執行加1操作。也可以傳遞已處理記錄的數目或已寫入的字節數。在任何線程中都可以調用dispatch_source_merge_data 。需要注意的是,不可以傳遞0值(事件不會被觸發),同樣也不可以傳遞負數。

GCD真的不能像OperationQueue那樣終止任務?

完整例子Demo1:讓 Dispatch Source “幫” Dispatch Queue 實現暫停和恢復功能

本節配套代碼在 Demo1 中(Demo_01_對DispatchSource實現取消恢復操作_main隊列版)。

先寫一段代碼演示下DispatchSource的基本用法:

//
//  .m
//  CYLDispatchSourceTest
//
//  Created by 微博@iOS程序犭袁( http://weibo.com/luohanchenyilong/) on 15/9/1.
//  Copyright (c) 2015年 https://github.com/ChenYilong . All rights reserved.
//

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.
    // 指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。設定Main Dispatch Queue 爲追加處理的Dispatch Queue
    _processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
                                                    dispatch_get_main_queue());
    __block NSUInteger totalComplete = 0;
    dispatch_source_set_event_handler(_processingQueueSource, ^{
        //當處理事件被最終執行時,計算後的數據可以通過dispatch_source_get_data來獲取。這個數據的值在每次響應事件執行後會被重置,所以totalComplete的值是最終累積的值。
        NSUInteger value = dispatch_source_get_data(_processingQueueSource);
        totalComplete += value;
        NSLog(@"進度:%@", @((CGFloat)totalComplete/100));
        NSLog(@"
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章