416 строки
11 KiB
Plaintext
416 строки
11 KiB
Plaintext
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTTiming.h"
|
|
|
|
#import <FBReactNativeSpec/FBReactNativeSpec.h>
|
|
|
|
#import <React/RCTAssert.h>
|
|
#import <React/RCTBridge+Private.h>
|
|
#import <React/RCTBridge.h>
|
|
#import <React/RCTConvert.h>
|
|
#import <React/RCTLog.h>
|
|
#import <React/RCTUtils.h>
|
|
|
|
#import "CoreModulesPlugins.h"
|
|
|
|
static const NSTimeInterval kMinimumSleepInterval = 1;
|
|
|
|
// These timing contants should be kept in sync with the ones in `JSTimers.js`.
|
|
// The duration of a frame. This assumes that we want to run at 60 fps.
|
|
static const NSTimeInterval kFrameDuration = 1.0 / 60.0;
|
|
// The minimum time left in a frame to trigger the idle callback.
|
|
static const NSTimeInterval kIdleCallbackFrameDeadline = 0.001;
|
|
|
|
@interface _RCTTimer : NSObject
|
|
|
|
@property (nonatomic, strong, readonly) NSDate *target;
|
|
@property (nonatomic, assign, readonly) BOOL repeats;
|
|
@property (nonatomic, copy, readonly) NSNumber *callbackID;
|
|
@property (nonatomic, assign, readonly) NSTimeInterval interval;
|
|
|
|
@end
|
|
|
|
@implementation _RCTTimer
|
|
|
|
- (instancetype)initWithCallbackID:(NSNumber *)callbackID
|
|
interval:(NSTimeInterval)interval
|
|
targetTime:(NSTimeInterval)targetTime
|
|
repeats:(BOOL)repeats
|
|
{
|
|
if ((self = [super init])) {
|
|
_interval = interval;
|
|
_repeats = repeats;
|
|
_callbackID = callbackID;
|
|
_target = [NSDate dateWithTimeIntervalSinceNow:targetTime];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
/**
|
|
* Returns `YES` if we should invoke the JS callback.
|
|
*/
|
|
- (BOOL)shouldFire:(NSDate *)now
|
|
{
|
|
if (_target && [_target timeIntervalSinceDate:now] <= 0) {
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)reschedule
|
|
{
|
|
// The JS Timers will do fine grained calculating of expired timeouts.
|
|
_target = [NSDate dateWithTimeIntervalSinceNow:_interval];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface _RCTTimingProxy : NSObject
|
|
|
|
@end
|
|
|
|
// NSTimer retains its target, insert this class to break potential retain cycles
|
|
@implementation _RCTTimingProxy {
|
|
__weak id _target;
|
|
}
|
|
|
|
+ (instancetype)proxyWithTarget:(id)target
|
|
{
|
|
_RCTTimingProxy *proxy = [self new];
|
|
if (proxy) {
|
|
proxy->_target = target;
|
|
}
|
|
return proxy;
|
|
}
|
|
|
|
- (void)timerDidFire
|
|
{
|
|
[_target timerDidFire];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTTiming {
|
|
NSMutableDictionary<NSNumber *, _RCTTimer *> *_timers;
|
|
NSTimer *_sleepTimer;
|
|
BOOL _sendIdleEvents;
|
|
BOOL _inBackground;
|
|
id<RCTTimingDelegate> _timingDelegate;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
@synthesize paused = _paused;
|
|
@synthesize pauseCallback = _pauseCallback;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
- (instancetype)initWithDelegate:(id<RCTTimingDelegate>)delegate
|
|
{
|
|
if (self = [super init]) {
|
|
[self setup];
|
|
_timingDelegate = delegate;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)initialize
|
|
{
|
|
[self setup];
|
|
}
|
|
|
|
- (void)setup
|
|
{
|
|
_paused = YES;
|
|
_timers = [NSMutableDictionary new];
|
|
_inBackground = NO;
|
|
|
|
for (NSString *name in @[
|
|
UIApplicationWillResignActiveNotification,
|
|
UIApplicationDidEnterBackgroundNotification,
|
|
UIApplicationWillTerminateNotification
|
|
]) {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(appDidMoveToBackground)
|
|
name:name
|
|
object:nil];
|
|
}
|
|
|
|
for (NSString *name in @[ UIApplicationDidBecomeActiveNotification, UIApplicationWillEnterForegroundNotification ]) {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(appDidMoveToForeground)
|
|
name:name
|
|
object:nil];
|
|
}
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_sleepTimer invalidate];
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return RCTJSThread;
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
[self stopTimers];
|
|
_bridge = nil;
|
|
_timingDelegate = nil;
|
|
}
|
|
|
|
- (void)appDidMoveToBackground
|
|
{
|
|
// Deactivate the CADisplayLink while in the background.
|
|
[self stopTimers];
|
|
_inBackground = YES;
|
|
|
|
// Issue one final timer callback, which will schedule a
|
|
// background NSTimer, if needed.
|
|
[self didUpdateFrame:nil];
|
|
}
|
|
|
|
- (void)appDidMoveToForeground
|
|
{
|
|
_inBackground = NO;
|
|
[self startTimers];
|
|
}
|
|
|
|
- (void)stopTimers
|
|
{
|
|
if (_inBackground) {
|
|
return;
|
|
}
|
|
|
|
if (!_paused) {
|
|
_paused = YES;
|
|
if (_pauseCallback) {
|
|
_pauseCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)startTimers
|
|
{
|
|
if ((!_bridge && !_timingDelegate) || _inBackground || ![self hasPendingTimers]) {
|
|
return;
|
|
}
|
|
|
|
if (_paused) {
|
|
_paused = NO;
|
|
if (_pauseCallback) {
|
|
_pauseCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)hasPendingTimers
|
|
{
|
|
@synchronized(_timers) {
|
|
return _sendIdleEvents || _timers.count > 0;
|
|
}
|
|
}
|
|
|
|
- (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
|
|
@synchronized(_timers) {
|
|
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"];
|
|
if (_bridge) {
|
|
[_bridge enqueueJSCall:@"JSTimers" method:@"callTimers" args:@[ sortedTimers ] completion:NULL];
|
|
} else {
|
|
[_timingDelegate callTimers:sortedTimers];
|
|
}
|
|
}
|
|
|
|
for (_RCTTimer *timer in timersToCall) {
|
|
if (timer.repeats) {
|
|
[timer reschedule];
|
|
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
|
|
} else {
|
|
@synchronized(_timers) {
|
|
[_timers removeObjectForKey:timer.callbackID];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_sendIdleEvents) {
|
|
NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
|
|
NSTimeInterval frameElapsed = currentTimestamp - update.timestamp;
|
|
if (kFrameDuration - frameElapsed >= kIdleCallbackFrameDeadline) {
|
|
NSNumber *absoluteFrameStartMS = @((currentTimestamp - frameElapsed) * 1000);
|
|
if (_bridge) {
|
|
[_bridge enqueueJSCall:@"JSTimers" method:@"callIdleCallbacks" args:@[ absoluteFrameStartMS ] completion:NULL];
|
|
} else {
|
|
[_timingDelegate callIdleCallbacks:absoluteFrameStartMS];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
NSUInteger timerCount;
|
|
@synchronized(_timers) {
|
|
timerCount = _timers.count;
|
|
}
|
|
if (_inBackground) {
|
|
if (timerCount) {
|
|
[self scheduleSleepTimer:nextScheduledTarget];
|
|
}
|
|
} else 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 (timerCount == 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)scheduleSleepTimer:(NSDate *)sleepTarget
|
|
{
|
|
@synchronized(self) {
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)timerDidFire
|
|
{
|
|
_sleepTimer = nil;
|
|
if (_paused) {
|
|
[self startTimers];
|
|
|
|
// Immediately dispatch frame, so we don't have to wait on the displaylink.
|
|
[self didUpdateFrame:nil];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A method used for asynchronously creating a timer. If the timer has already expired,
|
|
* (based on the provided jsSchedulingTime) then it will be immediately invoked.
|
|
*
|
|
* 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
|
|
: (double)callbackID duration
|
|
: (NSTimeInterval)jsDuration jsSchedulingTime
|
|
: (double)jsSchedulingTime repeats
|
|
: (BOOL)repeats)
|
|
{
|
|
NSNumber *callbackIdObjc = [NSNumber numberWithDouble:callbackID];
|
|
NSDate *schedulingTime = [RCTConvert NSDate:[NSNumber numberWithDouble:jsSchedulingTime]];
|
|
if (jsDuration == 0 && repeats == NO) {
|
|
// For super fast, one-off timers, just enqueue them immediately rather than waiting a frame.
|
|
if (_bridge) {
|
|
[_bridge _immediatelyCallTimer:callbackIdObjc];
|
|
} else {
|
|
[_timingDelegate immediatelyCallTimer:callbackIdObjc];
|
|
}
|
|
return;
|
|
}
|
|
|
|
[self createTimerForNextFrame:callbackIdObjc duration:jsDuration jsSchedulingTime:schedulingTime repeats:repeats];
|
|
}
|
|
|
|
/**
|
|
* A method used for synchronously creating a timer. The timer will not be invoked until the
|
|
* next frame, regardless of whether it has already expired (i.e. jsSchedulingTime is 0).
|
|
*/
|
|
- (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID
|
|
duration:(NSTimeInterval)jsDuration
|
|
jsSchedulingTime:(NSDate *)jsSchedulingTime
|
|
repeats:(BOOL)repeats
|
|
{
|
|
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];
|
|
@synchronized(_timers) {
|
|
_timers[callbackID] = timer;
|
|
}
|
|
|
|
if (_inBackground) {
|
|
[self scheduleSleepTimer:timer.target];
|
|
} else if (_paused) {
|
|
if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) {
|
|
[self scheduleSleepTimer:timer.target];
|
|
} else {
|
|
[self startTimers];
|
|
}
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(deleteTimer : (double)timerID)
|
|
{
|
|
@synchronized(_timers) {
|
|
[_timers removeObjectForKey:[NSNumber numberWithDouble:timerID]];
|
|
}
|
|
if (![self hasPendingTimers]) {
|
|
[self stopTimers];
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setSendIdleEvents : (BOOL)sendIdleEvents)
|
|
{
|
|
_sendIdleEvents = sendIdleEvents;
|
|
if (sendIdleEvents) {
|
|
[self startTimers];
|
|
} else if (![self hasPendingTimers]) {
|
|
[self stopTimers];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
Class RCTTimingCls(void)
|
|
{
|
|
return RCTTiming.class;
|
|
}
|