美文网首页
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