ReactNative 中 ObjC 多線程編程 GCD 的運用

此文成于2016/03/17,所以比較老了

怎么樣得到一個線程隊列?

  1. 要么從系統(tǒng)中取.
  2. 要么自己創(chuàng)建

系統(tǒng)中的線程隊列

有兩個方法:

  1. dispatch_get_global_queue 通過此方法獲得大家熟悉的 并發(fā)線程隊列.
    根據(jù)服務(wù)的優(yōu)先級從高到低5個: QOS_CLASS_USER_INTERACTIVE,QOS_CLASS_USER_INITIATED,QOS_CLASS_DEFAULT,QOS_CLASS_UTILITY,QOS_CLASS_BACKGROUND

  2. dispatch_get_main_queue 這是應(yīng)用在 main() 方法還沒有調(diào)用就已經(jīng)創(chuàng)建好附加到應(yīng)用線程中的線程隊列.
    一般說的 UI線程,與 UI 相關(guān)的操作需要在 UI線程中執(zhí)行才會有正確的響應(yīng).
    值得說明的是,上面的全局隊列是,并發(fā)的. UI 線程隊列是串行的.

創(chuàng)建線程隊列

創(chuàng)建線程使用:

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

隊列的屬性目前只有一個就是是否是并發(fā)的.
如果是串行的,參數(shù)使用 宏 DISPATCH_QUEUE_SERIAL
如果是并行的,參數(shù)使用 宏 DISPATCH_QUEUE_CONCURRENT

在 RN 中共有 9 個地方創(chuàng)建了不同作用的自定義的線程隊列:

  1. 在 RCTImageLoader.m 中創(chuàng)建的 URL 緩存串行隊列.
  // All access to URL cache must be serialized
  if (!_URLCacheQueue) {
    _URLCacheQueue = dispatch_queue_create("com.facebook.react.ImageLoaderURLCacheQueue", DISPATCH_QUEUE_SERIAL);
  }
  1. 在 RCTSRWebSocket.m 中創(chuàng)建的 串行工作隊列.
  _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
  1. 在 RCTWebSocketExecutor.m 創(chuàng)建的 JS 執(zhí)行串行隊列.
  _jsQueue = dispatch_queue_create("com.facebook.React.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
  1. RCTAsyncLocalStorage 中創(chuàng)建的 單例的 LocalStorage 操作的串行隊列.
static dispatch_queue_t RCTGetMethodQueue()
{
  // We want all instances to share the same queue since they will be reading/writing the same files.
  static dispatch_queue_t queue;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    queue = dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
  });
  return queue;
}
  1. 在 RCTBatchedBridge.m 中創(chuàng)建的 JS 與 Native 通信 Bridge 的并行隊列.
  dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT);
  1. 在 RCTModuleData.m 中當(dāng)模塊沒有指定methodQueue時 創(chuàng)建的用于模塊中公開的方法執(zhí)行的串行的隊列.
     // Create new queue (store queueName, as it isn't retained by dispatch_queue)
      _queueName = [NSString stringWithFormat:@"com.facebook.React.%@Queue", self.name];
      _methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
  1. 在 RCTUIManager.m 中創(chuàng)建的用于 RN 中 維護布局及更新布局的最高優(yōu)先級的串行隊列.
      dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
      _shadowQueue = dispatch_queue_create(queueName, attr);
  1. 在 RCTProfile.m 中創(chuàng)建的全局的用于 Profile 的 串行隊列
dispatch_queue_t RCTProfileGetQueue(void)
{
  static dispatch_queue_t queue;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    queue = dispatch_queue_create("com.facebook.react.Profiler", DISPATCH_QUEUE_SERIAL);
  });
  return queue;
}
  1. 在 RCTPerfMonitor.m 中 創(chuàng)建的用于異步IO的串行隊列.
 _queue = dispatch_queue_create("com.facebook.react.RCTPerfMonitor", DISPATCH_QUEUE_SERIAL);

簡單的異步

直接就用 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{}); 即可.

  1. 異步 IO, 例如從本地讀取 JS Bundle.

例如 (RCTJavaScriptLoader.m#loadBundleAtURL:onComplete:) 方法中,當(dāng)判斷出 NSURL 是指向本地的 JS Bundle 時,aync 一個從本地加載資源的 block.

    NSString *filePath = scriptURL.path;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSError *error = nil;
      NSData *source = [NSData dataWithContentsOfFile:filePath
                                              options:NSDataReadingMappedIfSafe
                                                error:&error];
      RCTPerformanceLoggerSet(RCTPLBundleSize, source.length);
      onComplete(error, source);
    });

定時執(zhí)行

GCD 中定義執(zhí)行,一般使用 dispatch_after

void
dispatch_after(dispatch_time_t when,
    dispatch_queue_t queue,
    dispatch_block_t block);

RN 只在處理加載完成時 UI 的切換時使用. 用以支持 _loadingViewFadeDelay 配置項.

dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_loadingViewFadeDelay * NSEC_PER_SEC));
dispatch_after(when,dispatch_get_main_queue(), ^{
  // TransitionCrossDissolve
}

dispatch_time_t

一般情況下 dispatch_time_t 都需要通過 dispatch_time 函數(shù)來構(gòu)造.
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
它的參數(shù)中 when 為 0 時表示現(xiàn)在 DISPATCH_TIME_NOW 是一個更可讀的宏定義.
#define DISPATCH_TIME_NOW (0ull)

  • 默認(rèn)時間是基于 mach_absolute_time
  • 單位是: nano seconds 納秒.

NSEC_PER_SEC 給出了一秒等于多少納秒的宏定義. 宏名稱相當(dāng)于是 nano_seconds_per_second

#define NSEC_PER_SEC 1000000000ull

所以上面的代碼實現(xiàn)了延遲 _loadingViewFadeDelay 秒之后之后執(zhí)行的功能.

只執(zhí)行一次

void
dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

可以這樣代碼的寫法可以說是, ObjC 中 線程安全的延遲初始化的定勢寫法了.

    static NSDateFormatter *formatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      formatter = [NSDateFormatter new];
      formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ";
      formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
      formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
    });

在 RN 的代碼中有 34 處 dispatch_once的使用.

  • dispatch_once_t 實際是一個long typedef long dispatch_once_t;
    一般要求初始值為 0,不過, 靜態(tài)或者全局的變量初始默認(rèn)值都是0.
  • A predicate for use with dispatch_once(). It must be initialized to zero.
  • Note: static and global variables default to zero.
  • 局部靜態(tài)變量, 像最終的 formatter,onceToken 在不同的調(diào)用中,雖然它是局部的,但是其實是全局變量,只不過聲明在局部是局部可見的全局變量. 它跟 dispatch_once 的結(jié)合使用實現(xiàn)了,線程安全的延遲單例.

NSThread 線程與消息循環(huán)

在 RCTJSCExecutor.m 實例的創(chuàng)建默認(rèn)初始過程,就使用創(chuàng)建 JS執(zhí)行的線程.

- (instancetype)init
{
  NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
                                                       selector:@selector(runRunLoopThread)
                                                         object:nil];
  javaScriptThread.name = @"com.facebook.React.JavaScript";

  if ([javaScriptThread respondsToSelector:@selector(setQualityOfService:)]) {
    [javaScriptThread setQualityOfService:NSOperationQualityOfServiceUserInteractive];
  } else {
    javaScriptThread.threadPriority = [NSThread mainThread].threadPriority;
  }

  [javaScriptThread start];

  return [self initWithJavaScriptThread:javaScriptThread context:nil];
}

而其中的 runRunLoopThread 方法中則將此線程變成了一個消息循環(huán)線程,沒有消息時在等著有就馬上處理.

+ (void)runRunLoopThread
{
  @autoreleasepool {
    // copy thread name to pthread name
    pthread_setname_np([NSThread currentThread].name.UTF8String);

    // Set up a dummy runloop source to avoid spinning
    CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
    CFRelease(noSpinSource);

    // run the run loop
    while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
      RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
    }
  }
}

要明白上面的代碼,首先就是要理解, RunLoop, 線程與 RunLoop 的關(guān)系.

  1. 線程,我們一般用來執(zhí)行某一段任務(wù).執(zhí)行完就退出了.
  2. 如果我們不想讓線程馬上退出,讓等待我們命令然后再執(zhí)行任務(wù). 我們應(yīng)該怎么做呢?
    最簡單的思維就是在線程類中有一個任務(wù)隊列,線程時不時的去檢查一下這個隊列,如果隊列中有任務(wù)就執(zhí)行,如果沒有就消息一下,再去檢查 .
  3. 上面的方法邏輯上是沒有問題的,但是對于消息時間不好控制,休息的太久了,比如 1 秒,那么我們?nèi)蝿?wù)執(zhí)行就都可能會有 1 秒的延遲. 怎么樣才能把這個 延遲減少呢?
  4. 怎么樣? 我們希望當(dāng)有任務(wù)來時馬上就得到提供,然后從休息中中斷出來.馬上執(zhí)行這個任務(wù).
  5. 好了, 那代碼怎么寫呢?
  6. 答案是 結(jié)合 RunLoop

看下文檔說 RunLoop 是什么.

A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
一句話,有任務(wù)時努力工作,沒任務(wù)時好好休息. 這就是 RunLoop.

然后,我們并不能創(chuàng)建 RunLoop,而是系統(tǒng)在創(chuàng)建 NSThread 時就為我們創(chuàng)建好了,包含應(yīng)用的 MainThread.

對于 RunLoop OC 中有兩層的 API 可用,高層的 NSRunLoop 類,
底層的 C 系的CFRunLoop系列.

得到當(dāng)前線程的 RunLoop 使用 NSRunLoop.currentRunLoop() 或者. CFRunLoopGetCurrent()

下面繼續(xù)問題:

  1. 怎么給 RunLoop 指派任務(wù) ?
    RunLoop 把任務(wù)的來源稱為 Source.
    在 RN 的 jsThread 任務(wù)來源是 : Cocoa 框架的 performSelector 系列的API.
    我們通過 performSelector 給 RunLoop 派發(fā)任務(wù).
    如下:
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
if ([NSThread currentThread] != _javaScriptThread) {
  [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
               onThread:_javaScriptThread withObject:block waitUntilDone:NO];
} else {
  block();
}
}
  1. 怎么樣啟動 RunLoop?
    通過某一個 run 方法來啟動.
  • NSRunLoop - (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate
  • CFRunLoop CFRunLoopRunResult CFRunLoopRunInMode ( CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled );
    RN 中上面的代碼是這樣子的 :
CFRunLoopRunInMode(kCFRunLoopDefaultMode, ([NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)
  1. 啟動之后為什么后面的異步消息沒有執(zhí)行?
    一般來說有兩個原因:
  2. seconds 設(shè)置得太少了.
  3. 沒有添加 sourceNSRunLoop 文檔中是有明確的說明的:

If no input sources or timers are attached to the run loop, this method exits immediately;
所以上面 RN 的代碼中
// Set up a dummy runloop source to avoid spinning
就是針對這個問題的.

  1. NSRunLoop中 Timer 也是一個合法的 Source
    所以在 RCTSRWebSocket.m 中是通過不回一個永遠(yuǎn)不觸發(fā)的 Timer 來添加一個 dummy runloop source
    的, 如下:
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:self selector:@selector(step) userInfo:nil repeats:NO];
  [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];

線程隊列分組與等等

在 RN 代碼中 (React/Base/RCTBatchedBridge.m#start) 方法中就有大量的使用
dispatch_group 來協(xié)調(diào)多個異步調(diào)用的.

在線程中如果按現(xiàn)實中一個組分別完成一個任務(wù)的某一個部分來理解可能更好一點.

  1. 首先是創(chuàng)建一個分組:
    dispatch_group_t initModulesAndLoadSource = dispatch_group_create();

  2. 告訴系統(tǒng)有多少人去執(zhí)行任務(wù). 因為系統(tǒng)不知道有多少人去執(zhí)行任務(wù).
    因為系統(tǒng)后面需要點數(shù),以判斷是否所有人的任務(wù)都完成了.
    通過
    dispatch_group_enter(initModulesAndLoadSource);
    來告訴系統(tǒng)有一個人派出去執(zhí)行任務(wù)了.

  3. 通過系統(tǒng)有一個人已經(jīng)完成任務(wù)了.
    dispatch_group_leave(initModulesAndLoadSource);
    enterleave 是成對出現(xiàn)了, 調(diào)用一次 enter 表示未完成任務(wù)數(shù)加1,
    調(diào)用一次 leave 表示未完成任務(wù)減1.當(dāng)未完成任務(wù)數(shù)為0時,表示所有的分組任務(wù)都完成了.

如下是 RN 中異步加載 Bundle 代碼的任務(wù)執(zhí)行代碼:

 // Asynchronously load source code
 dispatch_group_enter(initModulesAndLoadSource);
 __weak RCTBatchedBridge *weakSelf = self;
 __block NSData *sourceCode;
 [self loadSource:^(NSError *error, NSData *source) {
   if (error) {
     dispatch_async(dispatch_get_main_queue(), ^{
       [weakSelf stopLoadingWithError:error];
     });
   }

   sourceCode = source;
   dispatch_group_leave(initModulesAndLoadSource);
 }];

像這種要求成對出現(xiàn) enter,leave 寫法,可能比較容易出錯,因為有時會忘了寫,特別是代碼比較長的時候.
有沒有辦法可以讓封裝一下這種代碼呢?
好消息是,系統(tǒng)已經(jīng)有封裝好的方法了.
那就是 dispatch_group_async

   void
   dispatch_group_async(dispatch_group_t group,
                            dispatch_queue_t queue,
                            dispatch_block_t block);
   ```
通過此函數(shù),系統(tǒng)自動處理了 `enter`,`leave`的處理.

4. 任務(wù)完成時通知我.
系統(tǒng)中提供了 `dispatch_group_notify` 函數(shù) .

```objc
void
dispatch_group_notify(dispatch_group_t group,
   dispatch_queue_t queue,
   dispatch_block_t block);

任務(wù)完成之后會執(zhí)行上面提供的block 塊.

RN 中 直接使用這種 dispatch_group_asyncdispatch_group_notify 是在
加載模塊配置及構(gòu)造JS 執(zhí)行環(huán)境中有使用:

   dispatch_group_t setupJSExecutorAndModuleConfig = dispatch_group_create();
   // Asynchronously initialize the JS executor
   dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
     [weakSelf setUpExecutor];
   });
   // Asynchronously gather the module config
   dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
     if (weakSelf.isValid) {
       config = [weakSelf moduleConfig];
     }
   });
   dispatch_group_notify(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
     [weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) {
       if (error) {
         dispatch_async(dispatch_get_main_queue(), ^{
           [weakSelf stopLoadingWithError:error];
         });
       }
     }];
   
   });

主要結(jié)構(gòu)如下,部分代碼有刪減.

dipatch_group_asyncdispatch_group_enter / dispatch_group_leave 完成同樣的功能時.
dispatch_group_async 更為方便一些, 但是 enter/leave的模式在與其他異步方法協(xié)調(diào)工作時會更靈活.
例如在 RN 中的 start 代碼就混用了上面的兩種方式.

  1. 等等直到任務(wù)完成
    除了上面的任務(wù)完成得到通知之外 , 系統(tǒng)還提供了一直等待任務(wù)完成的功能.
    使用 dispatch_group_wait
long
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

返回0表示任務(wù)完成, 非0 表示任務(wù)完成超時了.
此函數(shù)將一直阻塞直到任務(wù)完成或者超時:

Wait synchronously until all the blocks associated with a group have
completed or until the specified timeout has elapsed.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容