美文网首页
ReactNative 中 ObjC 多线程编程 GCD 的运用

ReactNative 中 ObjC 多线程编程 GCD 的运用

作者: 一半晴天 | 来源:发表于2018-06-16 16:27 被阅读14次

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

    怎么样得到一个线程队列?

    1. 要么从系统中取.
    2. 要么自己创建

    系统中的线程队列

    有两个方法:

    1. dispatch_get_global_queue 通过此方法获得大家熟悉的 并发线程队列.
      根据服务的优先级从高到低5个: QOS_CLASS_USER_INTERACTIVE,QOS_CLASS_USER_INITIATED,QOS_CLASS_DEFAULT,QOS_CLASS_UTILITY,QOS_CLASS_BACKGROUND

    2. dispatch_get_main_queue 这是应用在 main() 方法还没有调用就已经创建好附加到应用线程中的线程队列.
      一般说的 UI线程,与 UI 相关的操作需要在 UI线程中执行才会有正确的响应.
      值得说明的是,上面的全局队列是,并发的. UI 线程队列是串行的.

    创建线程队列

    创建线程使用:

    dispatch_queue_t
    dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
    

    队列的属性目前只有一个就是是否是并发的.
    如果是串行的,参数使用 宏 DISPATCH_QUEUE_SERIAL
    如果是并行的,参数使用 宏 DISPATCH_QUEUE_CONCURRENT

    在 RN 中共有 9 个地方创建了不同作用的自定义的线程队列:

    1. 在 RCTImageLoader.m 中创建的 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 中创建的 串行工作队列.
      _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    
    1. 在 RCTWebSocketExecutor.m 创建的 JS 执行串行队列.
      _jsQueue = dispatch_queue_create("com.facebook.React.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
    
    1. RCTAsyncLocalStorage 中创建的 单例的 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 中创建的 JS 与 Native 通信 Bridge 的并行队列.
      dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    1. 在 RCTModuleData.m 中当模块没有指定methodQueue时 创建的用于模块中公开的方法执行的串行的队列.
         // 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 中创建的用于 RN 中 维护布局及更新布局的最高优先级的串行队列.
          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 中创建的全局的用于 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 中 创建的用于异步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:) 方法中,当判断出 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);
        });
    

    定时执行

    GCD 中定义执行,一般使用 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 函数来构造.
    dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
    它的参数中 when 为 0 时表示现在 DISPATCH_TIME_NOW 是一个更可读的宏定义.
    #define DISPATCH_TIME_NOW (0ull)

    • 默认时间是基于 mach_absolute_time
    • 单位是: nano seconds 纳秒.

    NSEC_PER_SEC 给出了一秒等于多少纳秒的宏定义. 宏名称相当于是 nano_seconds_per_second

    #define NSEC_PER_SEC 1000000000ull

    所以上面的代码实现了延迟 _loadingViewFadeDelay 秒之后之后执行的功能.

    只执行一次

    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,不过, 静态或者全局的变量初始默认值都是0.
    • A predicate for use with dispatch_once(). It must be initialized to zero.
    • Note: static and global variables default to zero.
    • 局部静态变量, 像最终的 formatter,onceToken 在不同的调用中,虽然它是局部的,但是其实是全局变量,只不过声明在局部是局部可见的全局变量. 它跟 dispatch_once 的结合使用实现了,线程安全的延迟单例.

    NSThread 线程与消息循环

    在 RCTJSCExecutor.m 实例的创建默认初始过程,就使用创建 JS执行的线程.

    - (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 方法中则将此线程变成了一个消息循环线程,没有消息时在等着有就马上处理.

    + (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 的关系.

    1. 线程,我们一般用来执行某一段任务.执行完就退出了.
    2. 如果我们不想让线程马上退出,让等待我们命令然后再执行任务. 我们应该怎么做呢?
      最简单的思维就是在线程类中有一个任务队列,线程时不时的去检查一下这个队列,如果队列中有任务就执行,如果没有就消息一下,再去检查 .
    3. 上面的方法逻辑上是没有问题的,但是对于消息时间不好控制,休息的太久了,比如 1 秒,那么我们任务执行就都可能会有 1 秒的延迟. 怎么样才能把这个 延迟减少呢?
    4. 怎么样? 我们希望当有任务来时马上就得到提供,然后从休息中中断出来.马上执行这个任务.
    5. 好了, 那代码怎么写呢?
    6. 答案是 结合 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.
    一句话,有任务时努力工作,没任务时好好休息. 这就是 RunLoop.

    然后,我们并不能创建 RunLoop,而是系统在创建 NSThread 时就为我们创建好了,包含应用的 MainThread.

    对于 RunLoop OC 中有两层的 API 可用,高层的 NSRunLoop 类,
    底层的 C 系的CFRunLoop系列.

    得到当前线程的 RunLoop 使用 NSRunLoop.currentRunLoop() 或者. CFRunLoopGetCurrent()

    下面继续问题:

    1. 怎么给 RunLoop 指派任务 ?
      RunLoop 把任务的来源称为 Source.
      在 RN 的 jsThread 任务来源是 : Cocoa 框架的 performSelector 系列的API.
      我们通过 performSelector 给 RunLoop 派发任务.
      如下:
    - (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. 启动之后为什么后面的异步消息没有执行?
      一般来说有两个原因:
    2. seconds 设置得太少了.
    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 中是通过不回一个永远不触发的 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 来协调多个异步调用的.

    在线程中如果按现实中一个组分别完成一个任务的某一个部分来理解可能更好一点.

    1. 首先是创建一个分组:
      dispatch_group_t initModulesAndLoadSource = dispatch_group_create();

    2. 告诉系统有多少人去执行任务. 因为系统不知道有多少人去执行任务.
      因为系统后面需要点数,以判断是否所有人的任务都完成了.
      通过
      dispatch_group_enter(initModulesAndLoadSource);
      来告诉系统有一个人派出去执行任务了.

    3. 通过系统有一个人已经完成任务了.
      dispatch_group_leave(initModulesAndLoadSource);
      enterleave 是成对出现了, 调用一次 enter 表示未完成任务数加1,
      调用一次 leave 表示未完成任务减1.当未完成任务数为0时,表示所有的分组任务都完成了.

    如下是 RN 中异步加载 Bundle 代码的任务执行代码:

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

    像这种要求成对出现 enter,leave 写法,可能比较容易出错,因为有时会忘了写,特别是代码比较长的时候.
    有没有办法可以让封装一下这种代码呢?
    好消息是,系统已经有封装好的方法了.
    那就是 dispatch_group_async

       void
       dispatch_group_async(dispatch_group_t group,
                                dispatch_queue_t queue,
                                dispatch_block_t block);
       ```
    通过此函数,系统自动处理了 `enter`,`leave`的处理.
    
    4. 任务完成时通知我.
    系统中提供了 `dispatch_group_notify` 函数 .
    
    ```objc
    void
    dispatch_group_notify(dispatch_group_t group,
       dispatch_queue_t queue,
       dispatch_block_t block);
    

    任务完成之后会执行上面提供的block 块.

    RN 中 直接使用这种 dispatch_group_asyncdispatch_group_notify 是在
    加载模块配置及构造JS 执行环境中有使用:

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

    主要结构如下,部分代码有删减.

    dipatch_group_asyncdispatch_group_enter / dispatch_group_leave 完成同样的功能时.
    dispatch_group_async 更为方便一些, 但是 enter/leave的模式在与其他异步方法协调工作时会更灵活.
    例如在 RN 中的 start 代码就混用了上面的两种方式.

    1. 等等直到任务完成
      除了上面的任务完成得到通知之外 , 系统还提供了一直等待任务完成的功能.
      使用 dispatch_group_wait
    long
    dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    

    返回0表示任务完成, 非0 表示任务完成超时了.
    此函数将一直阻塞直到任务完成或者超时:

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

    相关文章

      网友评论

          本文标题:ReactNative 中 ObjC 多线程编程 GCD 的运用

          本文链接:https://www.haomeiwen.com/subject/arybeftx.html