深入浅出RN中的定时器(下)
2019-03-30 本文已影响0人
码农二哥
如果你想知道RN中定时器在Native端是如何驱动实现的,如果你对RN源码感兴趣或者有学习欲望的,如果你想知道setTimeout、setIdleCallback确切调用时机的话,不妨仔细看看本篇文章;如果你仅仅想停留在表面使用上,完全可以忽略本篇。如果不懂OC,可能看起来略微吃力。
RCTDisplayLink
先看看它的基本接口和基本属性:
@interface RCTDisplayLink : NSObject
{
CADisplayLink *_jsDisplayLink;
NSMutableSet<RCTModuleData *> *_frameUpdateObservers;
NSRunLoop *_runLoop;
}
- (instancetype)init;
- (void)invalidate;
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData;
- (void)addToRunLoop:(NSRunLoop *)runLoop;
@end
- 它的内部实现是靠CADisplayLink;
- 它在RNBridge启动的时候被添加在JS thread的NSRunLoopCommonModes,我们知道每一个RCTCxxBridge都有一个JS thread,_jsMessageThread这个thread其实还是RCTCxxBridge的JS thread,它们是在同一个runloop(_jsMessageThread借用JS thread的runloop封装了一套c++的接口),它的初始化如下:
// 这个代码是在js线程执行的,所以这里获取到的runloop就是js线程的runloop
_jsMessageThread = std::make_shared<RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
}
RCTDisplayLink的初始化
- RCTDisplayLink在initWithParentBridge中初始化,看代码:
- (instancetype)initWithParentBridge:(RCTBridge *)bridge
{
if ((self = [super initWithDelegate:bridge.delegate
bundleURL:bridge.bundleURL
moduleProvider:bridge.moduleProvider
launchOptions:bridge.launchOptions])) {
_parentBridge = bridge;
_performanceLogger = [bridge performanceLogger];
registerPerformanceLoggerHooks(_performanceLogger);
/**
* Set Initial State
*/
_valid = YES;
_loading = YES;
_pendingCalls = [NSMutableArray new];
_displayLink = [RCTDisplayLink new];
[RCTBridge setCurrentBridge:self];
}
return self;
}
DisplayLink被添加到runloop
- RCTDisplayLink在RN-JS框架代码加载完成后被添加到runloop中;
// executeSourceCode是在RN-Native框架初始化完成时调用
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync
{
// This will get called from whatever thread was actually executing JS.
dispatch_block_t completion = ^{
// 框架js已经执行完毕
#ifdef YRN_OPT
// 解决com.facebook.react.JavaScript 线程无法延时释放的问题
@autoreleasepool {
if ([self.delegate respondsToSelector:@selector(bundleJSDidExecuted:)]) {
[self.delegate bundleJSDidExecuted:self];
}
}
#endif
// Flush pending calls immediately so we preserve ordering
[self _flushPendingCalls];
// Perform the state update and notification on the main thread, so we can't run into
// timing issues with RCTRootView
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptDidLoadNotification
object:self->_parentBridge userInfo:@{@"bridge": self}];
// Starting the display link is not critical to startup, so do it last
[self ensureOnJavaScriptThread:^{
// Register the display link to start sending js calls after everything is setup
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
}];
});
};
if (sync) {
[self executeApplicationScriptSync:sourceCode url:self.bundleURL];
completion();
} else {
[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
}
}
RCTDisplayLink的销毁
- RCTDisplayLink在RCTCxxBridge invalidate的时候销毁,看代码:
- (void)invalidate
{
[self ensureOnJavaScriptThread:^{
[self->_displayLink invalidate];
self->_displayLink = nil;
}];
}
RCTDisplayLink用在了什么地方?
- RCTCxxBridge的registerModuleForFrameUpdates:withModuleData方法实现其实就是转给_displayLink实现的,看代码:
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData
{
[_displayLink registerModuleForFrameUpdates:module withModuleData:moduleData];
}
- 这样bridge的module就可以调用这个方法来注册自己(如果有必要),从而有机会干点事情;
- 那么什么地方会调用registerModuleForFrameUpdates方法呢?当然是native module初始化的时候,哦不对,确定的说是native module初始化完成的时候(_setupComplete);
- 另外想要在frameUpdate的时候干事情还是需要遵循一个协议的(RCTFrameUpdateObserver),RN中目前只有RCTTiming模块和RCTNavigator模块实现了该协议;
- RCTTiming我们下面很快会将到,它是JSTimer实现的关键;
- native module setupComplete是在RCTModuleData类中,看RCTModuleData:
RCTModuleData
- 在RCTModuleData的instance方法调用的时候会调用finishSetupForInstance,它继而会调用_bridge的registerModuleForFrameUpdates:withModuleData方法,从而把对应的native module注册为观察者(当然前提是满足条件的话);
- 注意:RCTModuleData的instance方法的调用不一定真的就是在这时候才实例化native module,native module可能早已经初始化了,只是还差一点点才算complete;
- RCTModuleData的instance方法调用时机目前有三个:gatherConstants和methodQueue被调用时,另外一处当然是在bridge初始化时直接调用了;
- 另外RCTModuleData并不是native module,而是它的一个包装类;
- native module是一个遵循RCTBridgeModule协议的类;
- (void)finishSetupForInstance
{
if (!_setupComplete && _instance) {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData finishSetupForInstance]", nil);
_setupComplete = YES;
[_bridge registerModuleForFrameUpdates:_instance withModuleData:self];
#ifdef YRN_OPT
// 解决com.facebook.react.JavaScript 线程无法延时释放的问题
@autoreleasepool {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDidInitializeModuleNotification
object:_bridge
userInfo:@{@"module": _instance, @"bridge": RCTNullIfNil(_bridge.parentBridge)}];
}
#else
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDidInitializeModuleNotification
object:_bridge
userInfo:@{@"module": _instance, @"bridge": RCTNullIfNil(_bridge.parentBridge)}];
#endif
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
}
我们上面已经提到了RCTTiming遵循了RCTFrameUpdateObserver协议,满足了作为RCTDisplayLink观察者的条件,下面我们就来具体说说RCTTIming这个native module;
RCTTiming
- 它的包装者RCTModuleData的instance方法是在rn-js调用RCTiming的createTimer方法时触发调用的(因为createTimer调用需要在methodQueue中,从而触发了instance方法的调用);
- 但RCTTiming本身的实例化是在bridge初始化时通过调用RCTModuleData的initWithModuleInstance:bridge方法时就完成了,但还差一点点才算彻底setupComplete;
- RCTTiming实现了RCTFrameUpdateObserver协议;
- RCTTiming在某些时机会 startTimers(比如app处于actived状态时),在某些时机又会stopTimers(比如app处于非actived状态时),RCTTiming有个属性_paused标示当前自己的状态是start还是stop。无论是start还是stop都会调用_pauseCallback回调函数,_pauseCallback这个回调函数其实用来更新RCTDisplayLink的状态的(是暂停还是不暂停),我们下面就会说到;
- RCTTiming中RCTFrameUpdateObserver的协议方法didUpdateFrame的执行由RCTDisplayLink驱动,所以也是在JS Thread,最后我们来看看这个函数的实现:
- (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
// 遍历需要触发回调的timers,并找出下一个最快要触发的timer的触发时间点;
// 每一个_RCTTimer都保存了js方传递过来的关于一个timer所需要的信息;
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
// 回调前先sort一下
if (timersToCall.count > 0) {
NSArray<NSNumber *> *sortedTimers = [[timersToCall sortedArrayUsingComparator:^(_RCTTimer *a, _RCTTimer *b) {
return [a.target compare:b.target];
}] valueForKey:@"callbackID"];
[_bridge enqueueJSCall:@"JSTimers"
method:@"callTimers"
args:@[sortedTimers]
completion:NULL];
}
// 如果有需要重复触发的timer,reschedule一下,并更新一下[下一个最快要触发的timer的触发时间点];
// 如果不需要重复触发,则直接移除调(因为上面刚刚已经回调了);
for (_RCTTimer *timer in timersToCall) {
if (timer.repeats) {
[timer reschedule];
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
} else {
[_timers removeObjectForKey:timer.callbackID];
}
}
// 如果需要idle回调,则判断当前是否idle
if (_sendIdleEvents) {
NSTimeInterval frameElapsed = (CACurrentMediaTime() - update.timestamp);
// 如果这一帧16ms还没用完(16ms-已逝时间>0.001)则发送idle回调;
// 如果这一帧剩余时间小于0.001,只能说明当前不idle;
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.
// 如果没有timer了,那么就切换到pause状态;如果很久之后才有timer触发(超过1s),直接换成NSTimer来提高性能;
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;
// 如果最近一个timer触发需要1s以上,则pause并切换到NSTimer可能会提高性能;
else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) {
[self scheduleSleepTimer:nextScheduledTarget];
_paused = YES;
}
}
}
- 所有的逻辑直接看上面代码中的注释,注释应该说的已经比较清楚了;
- 还记的pauseCallback吗?我们看看它的实现:
// RCTDisplayLink.m
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData
{
// 如果natigve module没有遵循RCTFrameUpdateObserver协议,则直接return
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;
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.
if (![observer isPaused] && _runLoop) {
CFRunLoopPerformBlock([_runLoop getCFRunLoop], kCFRunLoopDefaultMode, ^{
[self updateJSDisplayLinkState];
});
}
}
- 我们之前说过:native module在setupComplete完成的时候会调用
registerModuleForFrameUpdates:withModuleData
这个方法来注册自己,从而有机会在屏幕刷新的时候干点事情; - 如果natigve module没有遵循RCTFrameUpdateObserver协议,则直接return;
- 如果native module遵循了RCTFrameUpdateObserver协议,则会给它设置一个pauseCallback,pauseCallback中主要做的事情是[weakSelf updateJSDisplayLinkState],我们来看看它的实现:
// RCTDisplayLink.m
- (void)updateJSDisplayLinkState
{
RCTAssertRunLoop();
BOOL pauseDisplayLink = YES;
for (RCTModuleData *moduleData in _frameUpdateObservers) {
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)moduleData.instance;
if (!observer.paused) {
pauseDisplayLink = NO;
break;
}
}
_jsDisplayLink.paused = pauseDisplayLink;
}
- 其实个函数主要是来更新_jsDisplayLink的状态的:如果所有的observers都是paused状态,则将_jsDisplayLink暂停,从而提高性能;
总结
- RN中的定时器JSTimer由RCTDisplayLink驱动;
- Native(RCTTiming)会在合适的时机(定时器触发时)调用JSTimer的callTimers方法;
- Native(RCTTiming)会在合适的时机(如果需要会在每一个16ms还有空闲时间的时候)调用JSTimer的callIdleCallbacks方法;
- RCTDisplayLink做了一些必要的优化(比如没有时暂停,超过1s没有需要触发的定时器时暂定RCTDisplayLink,用NSTimer代替以提高性能,减少浪费);