定时器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等,基本一致,不再描述。