美文网首页React Native
深入浅出RN中的定时器(下)

深入浅出RN中的定时器(下)

作者: 码农二哥 | 来源:发表于2019-03-30 13:42 被阅读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代替以提高性能,减少浪费);

    相关文章

      网友评论

        本文标题:深入浅出RN中的定时器(下)

        本文链接:https://www.haomeiwen.com/subject/udijbqtx.html