我们知道,RN在iOS上是通过JavascriptCore来执行JS代码以及OC与JS 互相调用的。 但是在看JavascriptCore源码的过程中,我发现没有关于定时器实现的代码(setTimeout、setInterval等),这让我产生了一个疑问,到底这个定时器到底是如何实现的?
带着这个疑问,我又看了WebKit的源码,发现在WebCore里面的setTimeout和setInterval,其本质是往runloop里面添加了一个CFRunLoopTimer,来执行定时任务,也就是说JS的定时器实际是通过原生的定时器来执行,那RN是不是也是这样呢?
通过在node_modules里全局搜索setTimeout关键字,我找到了一些线索, 见setupTimer.js
'use strict';
const {polyfillGlobal} = require('PolyfillFunctions');
/**
* Set up timers.
* You can use this module directly, or just require InitializeCore.
*/
const defineLazyTimer = name => {
polyfillGlobal(name, () => require('JSTimers')[name]);
};
defineLazyTimer('setTimeout');
defineLazyTimer('setInterval');
defineLazyTimer('setImmediate');
defineLazyTimer('clearTimeout');
defineLazyTimer('clearInterval');
defineLazyTimer('clearImmediate');
defineLazyTimer('requestAnimationFrame');
defineLazyTimer('cancelAnimationFrame');
defineLazyTimer('requestIdleCallback');
defineLazyTimer('cancelIdleCallback');
这个polyfillGlobal是往全局对象中注入了一个属性, 结合前面的代码意思就是当调用setTimeout的时候,相当于是调用了JSTimer的对应方法.
'use strict';
const defineLazyObjectProperty = require('defineLazyObjectProperty');
/**
* Sets an object's property. If a property with the same name exists, this will
* replace it but maintain its descriptor configuration. The property will be
* replaced with a lazy getter.
*
* In DEV mode the original property value will be preserved as `original[PropertyName]`
* so that, if necessary, it can be restored. For example, if you want to route
* network requests through DevTools (to trace them):
*
* global.XMLHttpRequest = global.originalXMLHttpRequest;
*
* @see https://github.com/facebook/react-native/issues/934
*/
function polyfillObjectProperty<T>(
object: Object,
name: string,
getValue: () => T,
): void {
const descriptor = Object.getOwnPropertyDescriptor(object, name);
if (__DEV__ && descriptor) {
const backupName = `original${name[0].toUpperCase()}${name.substr(1)}`;
Object.defineProperty(object, backupName, descriptor);
}
const {enumerable, writable, configurable} = descriptor || {};
if (descriptor && !configurable) {
console.error('Failed to set polyfill. ' + name + ' is not configurable.');
return;
}
defineLazyObjectProperty(object, name, {
get: getValue,
enumerable: enumerable !== false,
writable: writable !== false,
});
}
function polyfillGlobal<T>(name: string, getValue: () => T): void {
polyfillObjectProperty(global, name, getValue);
}
module.exports = {polyfillObjectProperty, polyfillGlobal};
这个JSTimer是什么呢?实际上是调用了Timing对象的creatTimer方法,参数包括回调函数callback、间隔事件duration、当前时间以及是否重复调用等。
/**
* JS implementation of timer functions. Must be completely driven by an
* external clock signal, all that's stored here is timerID, timer type, and
* callback.
*/
const JSTimers = {
/**
* @param {function} func Callback to be invoked after `duration` ms.
* @param {number} duration Number of milliseconds.
*/
setTimeout: function(func: Function, duration: number, ...args: any): number {
if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) {
console.warn(
ANDROID_LONG_TIMER_MESSAGE +
'\n' +
'(Saw setTimeout with duration ' +
duration +
'ms)',
);
}
const id = _allocateCallback(
() => func.apply(undefined, args),
'setTimeout',
);
Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
return id;
},
.....
//其他函数如setInterval等
}
这个Timing是通过桥接原生实现的,我们看下iOS这边的代码,RCTTiming.m
/**
* There's a small difference between the time when we call
* setTimeout/setInterval/requestAnimation frame and the time it actually makes
* it here. This is important and needs to be taken into account when
* calculating the timer's target time. We calculate this by passing in
* Date.now() from JS and then subtracting that from the current time here.
*/
RCT_EXPORT_METHOD(createTimer:(nonnull NSNumber *)callbackID
duration:(NSTimeInterval)jsDuration
jsSchedulingTime:(NSDate *)jsSchedulingTime
repeats:(BOOL)repeats)
{
if (jsDuration == 0 && repeats == NO) {
// For super fast, one-off timers, just enqueue them immediately rather than waiting a frame.
[_bridge _immediatelyCallTimer:callbackID];
return;
}
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) {
//下次执行的时间距离现在是否大于1秒
if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) {
//使用NSTimer
[self scheduleSleepTimer:timer.target];
} else {
//使用CADisplayLink
[self startTimers];
}
}
}
- (void)scheduleSleepTimer:(NSDate *)sleepTarget
{
if (!_sleepTimer || !_sleepTimer.valid) {
_sleepTimer = [[NSTimer alloc] initWithFireDate:sleepTarget
interval:0
target:[_RCTTimingProxy proxyWithTarget:self]
selector:@selector(timerDidFire)
userInfo:nil
repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode];
} else {
_sleepTimer.fireDate = [_sleepTimer.fireDate earlierDate:sleepTarget];
}
}
//sleeptimer的回调
- (void)timerDidFire
{
_sleepTimer = nil;
if (_paused) {
[self startTimers];
// Immediately dispatch frame, so we don't have to wait on the displaylink.
[self didUpdateFrame:nil];
}
}
- (void)startTimers
{
if (!_bridge || ![self hasPendingTimers]) {
return;
}
if (_paused) {
_paused = NO;
//这里pauseCallback是用来暂停或恢复displaylink定时器的
if (_pauseCallback) {
_pauseCallback();
}
}
}
这段代码是把JS这边传过来的计时器参数保存到RCTTimer对象中,然后启动了一个原生的定时器。原生的定时器有两种实现方式,一个是NSTimer,另一个是CADisplayLink。具体使用哪一个,判断的标准是,下次执行的时间距离现在是否大于1秒,如果大于1秒,则使用NSTimer,否则使用CADisplayLink。至于为什么这么做,主要是为了性能考虑,毕竟CADisplayLink一秒执行60次,如果时间精度要求没那么高的话就没必要了。
我们看下displayLink如何实现定时器的,这里根据pauseCallback关键字搜索到RCTFrameUpdateObserver协议
/**
* Protocol that must be implemented for subscribing to display refreshes (DisplayLink updates)
*/
@protocol RCTFrameUpdateObserver <NSObject>
/**
* Method called on every screen refresh (if paused != YES)
*/
- (void)didUpdateFrame:(RCTFrameUpdate *)update;
/**
* Synthesize and set to true to pause the calls to -[didUpdateFrame:]
*/
@property (nonatomic, readonly, getter=isPaused) BOOL paused;
/**
* Callback for pause/resume observer.
* Observer should call it when paused property is changed.
*/
@property (nonatomic, copy) dispatch_block_t pauseCallback;
@end
从这个协议的注释可以看出,pauseCallback是每次暂停或恢复定时器时调用的(开启定时器也算是恢复定时器),调用这个方法的时候会根据paused属性来判断是否回调,paused为true则回调didUpdateFrame方法。
为了详细了解其具体实现,我们继续搜索找到RCTDisplaylink类,我们可以看到RCTDisplaylink初始化的时候注册了一个displaylink的回调函数_jsThreadUpdate:。另外对外提供了一个方法registerModuleForFrameUpdates,这个方法是注册一个模块,每次屏幕刷新时都用displaylink的回调函数
- (instancetype)init
{
if ((self = [super init])) {
_frameUpdateObservers = [NSMutableSet new];
//注册了回调函数_jsThreadUpdate
_jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)];
}
return self;
}
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData
{
if (![moduleData.moduleClass conformsToProtocol:@protocol(RCTFrameUpdateObserver)] ||
[_frameUpdateObservers containsObject:moduleData]) {
return;
}
[_frameUpdateObservers addObject:moduleData];
// Don't access the module instance via moduleData, as this will cause deadlock
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)module;
__weak typeof(self) weakSelf = self;
//这里实现了pauseCallback
observer.pauseCallback = ^{
typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
CFRunLoopRef cfRunLoop = [strongSelf->_runLoop getCFRunLoop];
if (!cfRunLoop) {
return;
}
//确保是在当前的线程上执行
if ([NSRunLoop currentRunLoop] == strongSelf->_runLoop) {
[weakSelf updateJSDisplayLinkState];
} else {
CFRunLoopPerformBlock(cfRunLoop, kCFRunLoopDefaultMode, ^{
[weakSelf updateJSDisplayLinkState];
});
CFRunLoopWakeUp(cfRunLoop);
}
};
// Assuming we're paused right now, we only need to update the display link's state
// when the new observer is not paused. If it not paused, the observer will immediately
// start receiving updates anyway.
//当设置paused为true的时候,会立即执行一次回调
if (![observer isPaused] && _runLoop) {
CFRunLoopPerformBlock([_runLoop getCFRunLoop], kCFRunLoopDefaultMode, ^{
[self updateJSDisplayLinkState];
});
}
}
- (void)updateJSDisplayLinkState
{
RCTAssertRunLoop();
BOOL pauseDisplayLink = YES;
for (RCTModuleData *moduleData in _frameUpdateObservers) {
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)moduleData.instance;
if (!observer.paused) {
pauseDisplayLink = NO;
break;
}
}
//这里真正的执行了displayLink的暂停/恢复
_jsDisplayLink.paused = pauseDisplayLink;
}
这里可以看到传入的moduleData的实例实现了RCTFrameUpdateObserver协议,那这个moduleData具体是谁?没错,就是RCTTiming
@interface RCTTiming : NSObject <RCTBridgeModule, RCTInvalidating, RCTFrameUpdateObserver>
@end
//RCTFrameUpdateObserver的回调函数
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
NSDate *nextScheduledTarget = [NSDate distantFuture];
NSMutableArray<_RCTTimer *> *timersToCall = [NSMutableArray new];
NSDate *now = [NSDate date]; // compare all the timers to the same base time
for (_RCTTimer *timer in _timers.allValues) {
if ([timer shouldFire:now]) {
[timersToCall addObject:timer];
} else {
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
}
}
// Call timers that need to be called
if (timersToCall.count > 0) {
NSArray<NSNumber *> *sortedTimers = [[timersToCall sortedArrayUsingComparator:^(_RCTTimer *a, _RCTTimer *b) {
return [a.target compare:b.target];
}] valueForKey:@"callbackID"];
//timer到了指定的时间后回调js
[_bridge enqueueJSCall:@"JSTimers"
method:@"callTimers"
args:@[sortedTimers]
completion:NULL];
}
for (_RCTTimer *timer in timersToCall) {
if (timer.repeats) {
[timer reschedule];
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
} else {
[_timers removeObjectForKey:timer.callbackID];
}
}
if (_sendIdleEvents) {
NSTimeInterval frameElapsed = (CACurrentMediaTime() - update.timestamp);
if (kFrameDuration - frameElapsed >= kIdleCallbackFrameDeadline) {
NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSNumber *absoluteFrameStartMS = @((currentTimestamp - frameElapsed) * 1000);
[_bridge enqueueJSCall:@"JSTimers"
method:@"callIdleCallbacks"
args:@[absoluteFrameStartMS]
completion:NULL];
}
}
// Switch to a paused state only if we didn't call any timer this frame, so if
// in response to this timer another timer is scheduled, we don't pause and unpause
// the displaylink frivolously.
if (!_sendIdleEvents && timersToCall.count == 0) {
// No need to call the pauseCallback as RCTDisplayLink will ask us about our paused
// status immediately after completing this call
if (_timers.count == 0) {
_paused = YES;
}
// If the next timer is more than 1 second out, pause and schedule an NSTimer;
else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) {
[self scheduleSleepTimer:nextScheduledTarget];
_paused = YES;
}
}
}
这里enqueueJSCall是定时器回调后把结果通知到JS,JS这边再根据通过_callTimer函数及原生传过来的callbackId,找到对应的callback函数并回调给业务方
/**
* This is called from the native side. We are passed an array of timerIDs,
* and
*/
callTimers: function(timersToCall: Array<number>) {
invariant(
timersToCall.length !== 0,
'Cannot call `callTimers` with an empty list of IDs.',
);
// $FlowFixMe: optionals do not allow assignment from null
errors = null;
for (let i = 0; i < timersToCall.length; i++) {
_callTimer(timersToCall[i], 0);
}
if (errors) {
const errorCount = errors.length;
if (errorCount > 1) {
// Throw all the other errors in a setTimeout, which will throw each
// error one at a time
for (let ii = 1; ii < errorCount; ii++) {
JSTimers.setTimeout(
(error => {
throw error;
}).bind(null, errors[ii]),
0,
);
}
}
throw errors[0];
}
},
至此,我们完整了解到了RN定时器的原理,其本质是通过NSTimer或者CADisplayLink来实现的。
不过这里还有一个小细节:
- (void)setBridge:(RCTBridge *)bridge
{
RCTAssert(!_bridge, @"Should never be initialized twice!");
_paused = YES;
_timers = [NSMutableDictionary new];
for (NSString *name in @[UIApplicationWillResignActiveNotification,
UIApplicationDidEnterBackgroundNotification,
UIApplicationWillTerminateNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(stopTimers)
name:name
object:nil];
}
for (NSString *name in @[UIApplicationDidBecomeActiveNotification,
UIApplicationWillEnterForegroundNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(startTimers)
name:name
object:nil];
}
_bridge = bridge;
}
在RCTTiming设置bridge的时候,会注册两个通知,一个是APP退后台的,一个是APP回到前台的,分别触发stopTimers和startTimers函数暂停和恢复计时器。为什么要这么做,我猜测是退后台之后为了防止消耗太多电量以及大量占用cpu导致应用被杀掉,这跟H5的实现是一样的。但对开发者来说是一个坑(计时器被迫暂停),需要注意
- (void)stopTimers
{
if (!_paused) {
_paused = YES;
if (_pauseCallback) {
_pauseCallback();
}
}
}
- (void)startTimers
{
if (!_bridge || ![self hasPendingTimers]) {
return;
}
if (_paused) {
_paused = NO;
if (_pauseCallback) {
_pauseCallback();
}
}
}
另外还有一点需要注意的就是RCTDisplayLink是添加到在JS线程对应的runloop上的,所以当JS线程有大量计算时,会占用cpu资源,影响计时器的准确性。关于runloop的原理可以看下这篇博客https://blog.ibireme.com/2015/05/18/runloop/,里面有说到为什么计时器会不准。
网友评论