react-native源码——定时器

定时器RCTDisplayLink在程序启动时,随着RCTBatchedBridge一同初始化,但并没有立刻启动。当js文件加载完成,进入方法executeSourceCode

- (void)executeSourceCode:(NSData *)sourceCode
{
    ....  提取定时器相关代码
    NSRunLoop *targetRunLoop = [self->_javaScriptExecutor isKindOfClass:[RCTJSCExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop];
    [self->_displayLink addToRunLoop:targetRunLoop];
}

此时定时器被添加到RCTJSCExecutor文件中所创建的runloop中,这个runloop永不退出,是oc和js互相调用的线程,定时器启动。
定时器是CADisplayLink类型,每秒调用60次,绑定的方法是_jsThreadUpdate

- (void)_jsThreadUpdate:(CADisplayLink *)displayLink
{
  RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
  --------------------@1
  for (RCTModuleData *moduleData in _frameUpdateObservers) {
    id observer = (id)moduleData.instance;
    if (!observer.paused) {
      [self dispatchBlock:^{
        [observer didUpdateFrame:frameUpdate];   ------------ @3
      } queue:moduleData.methodQueue];
    }
  }。------------@2
  [self updateJSDisplayLinkState];
}

- (void)updateJSDisplayLinkState
{
  BOOL pauseDisplayLink = YES;
  for (RCTModuleData *moduleData in _frameUpdateObservers) {
    id observer = (id)moduleData.instance;
    if (!observer.paused) {
      pauseDisplayLink = NO;
      break;
    }
  }
  _jsDisplayLink.paused = pauseDisplayLink;
}

@1: 导出模块实例化完成之后,系统会查看该模块RCTModuleData是否遵守了RCTFrameUpdateObserver协议,若遵守,则会将该模块添加到定时器的_frameUpdateObservers属性之中,系统类RCTTiming和RCTNavigator实现了该协议。
定时器调用时,遍历_frameUpdateObservers中的模块实例对象,判断该对象的paused属性,若为NO,表明该对象处于启动状态,则调用它的didUpdateFrame更新状态。
@2: 由updateJSDisplayLinkState方法的意思是若_frameUpdateObservers中的所有实例对象的paused都是YES,那么就暂停当前定时器的运行,节约性能。

举例: 初始化一个新项目,运行。xcode控制台每隔2秒打印出

[] nw_connection_get_connected_socket_block_invoke 2553 Connection has no connected handler

分析: 定时器的调用频率是每秒60次,所以定时器肯定是被暂停过了。分析发现_jsThreadUpdate方法触发时,frameUpdateObservers内部只有一个RCTTiming模块的包装类RCTModuleData对象,由于模块的paused属性初始化为YES状态,故该方法什么都没做,并且进入updateJSDisplayLinkState方法,定时器置于暂停状态。那么又是什么触发了定时器再次执行嘞?
查看js端源码,在JSTimers.js中setTimeout处打断点,触发断点,调用堆栈如下

react-native源码——定时器_第1张图片

原因是websocket连接出错,触发websocketFailed这个事件的发生,进而触发了setTimeout。

  setTimeout: function(func: Function, duration: number, ...args?: any): number {
    const id = _allocateCallback(() => func.apply(undefined, args), 'setTimeout');
    Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
    return id;
  },

这里面的id就是原生端定时器触发回调函数的编号,唯一,详细后面分析。Timing.createTimer调用的是原生端Timing中的如下方法

RCT_EXPORT_METHOD(createTimer:(nonnull NSNumber *)callbackID
                  duration:(NSTimeInterval)jsDuration
                  jsSchedulingTime:(NSDate *)jsSchedulingTime
                  repeats:(BOOL)repeats)
{
  NSTimeInterval jsSchedulingOverhead = MAX(-jsSchedulingTime.timeIntervalSinceNow, 0);
  NSTimeInterval targetTime = jsDuration - jsSchedulingOverhead;
  if (jsDuration < 0.018) { // Make sure short intervals run each frame
    jsDuration = 0;
  }
  _RCTTimer *timer = [[_RCTTimer alloc] initWithCallbackID:callbackID
                                                  interval:jsDuration
                                                targetTime:targetTime
                                                   repeats:repeats];
  _timers[callbackID] = timer;
  if (_paused) { 
    if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) {
      [self scheduleSleepTimer:timer.target]; --------------------@3
    } else {
      [self startTimers]; --------------------@4
    }
  }
}

- (void)startTimers
{
  if (_paused) {
    _paused = NO;
    if (_pauseCallback) {
      _pauseCallback();
    }
  }
}

js端调用任何定时相关的函数都是触发的这个方法,例如setTimeout/setInterval/requestAnimation,setTimeout会进入@3处,再开启一个定时器延时,最终都是进入@4处。
@4: 将_paused属性设置为NO,调用_pauseCallback方法。_pauseCallback是在模块实例化的时候赋值的

react-native源码——定时器_第2张图片
- (void)registerModuleForFrameUpdates:(id)module
                       withModuleData:(RCTModuleData *)moduleData
{
  [_frameUpdateObservers addObject:moduleData];
  id observer = (id)module;
  __weak typeof(self) weakSelf = self;
  observer.pauseCallback = ^{
    typeof(self) strongSelf = weakSelf;
    if ([NSRunLoop currentRunLoop] == strongSelf->_runLoop) {
      [weakSelf updateJSDisplayLinkState];        -------------@2
    } else {
      CFRunLoopPerformBlock(cfRunLoop, kCFRunLoopDefaultMode, ^{
        [weakSelf updateJSDisplayLinkState];
      });
      CFRunLoopWakeUp(cfRunLoop);
    }
  };
}

进入@2方法,此时因为该模块_paused属性为NO,_jsDisplayLink.paused=NO,定时器激活,重新调用_jsThreadUpdate方法,打印输出。

结论就是:程序启动,初始化定时器,触发首次回调。由于内部模块RCTTiming的属性paused为YES,定时器没有任务需要执行,为了性能考虑,定时器置于暂停状态。2s后,js端因为websocket连接失败触发setTimeout方法,进而调用到原生端Timing模块createTimer方法,将属性paused设置为NO,调用_pauseCallback,进入updateJSDisplayLinkState,重新启动定时器,再触发定时器回调,控制台打印输出。简单来说就是下面两步不断循环
1、定时器激活 -> _jsThreadUpdate -> updateJSDisplayLinkState将定时器暂停
2、2s后js端websocket连接失败事件触发 -> setTimeout -> 原生端createTimer -> startTimers -> pauseCallback注册回调执行 -> updateJSDisplayLinkState将定时器激活

@3:

- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
    ........  提取相关代码
    [_bridge enqueueJSCall:@"JSTimersExecution"
                    method:@"callTimers"
                      args:@[timersToCall]
                completion:NULL];
}

原生端定时器执行js端回调事件,通过JavaScriptCore的上下文相关方法执行。enqueueJSCall会调用js端JSTimersExecution组件下面的callTimers方法,参数timersToCall就是js调用setTimeout时所传递给oc端的id,这里又传回给了js端,用来查找对应的回调函数执行。

查看setTimeout中id的生成方法

function _allocateCallback(func: Function, type: JSTimerType): number {
  const id = JSTimersExecution.GUID++;
  const freeIndex = _getFreeIndex();
  JSTimersExecution.timerIDs[freeIndex] = id;
  JSTimersExecution.callbacks[freeIndex] = func;
  JSTimersExecution.types[freeIndex] = type;
  return id;
}

id就是一个递增的数字,唯一。setTimeout方法的回调函数,id,类型等都被保存到对象JSTimersExecution中了。
原生端定时器回调时触发的js方法如下

  callTimers(timerIDs: [number]) {
    for (let i = 0; i < timerIDs.length; i++) {
      JSTimersExecution.callTimer(timerIDs[i], 0);
    }
  },

callTimer(timerID: number, frameTime: number) {
    const timerIndex = JSTimersExecution.timerIDs.indexOf(timerID);
    const callback = JSTimersExecution.callbacks[timerIndex];
    callback();
}

找到id对应的回调函数,执行。

综上: js端调用setTimeout函数,生成唯一id,将id和对应的回调函数保存在js端。传递参数id等进入原生端。原生端定时器启动,到时间后执行js端指定方法,并将id传回,进入js端。js端根据id找出对应的函数,并执行。

所有js端执行的定时方法,如setTimeout/setInterval/requestAnimation等,基本一致,不再描述。

你可能感兴趣的:(react-native源码——定时器)