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<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)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<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)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处打断点,触发断点,调用堆栈如下

原因是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是在模块实例化的时候赋值的

- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
                       withModuleData:(RCTModuleData *)moduleData
{
  [_frameUpdateObservers addObject:moduleData];
  id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)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等,基本一致,不再描述。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容