美文网首页
RN定时器在iOS端的实现

RN定时器在iOS端的实现

作者: FingerStyle | 来源:发表于2020-07-11 13:43 被阅读0次

    我们知道,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/,里面有说到为什么计时器会不准。

    相关文章

      网友评论

          本文标题:RN定时器在iOS端的实现

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