Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 71 additions & 25 deletions React/Modules/RCTTiming.m
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ @implementation RCTTiming
NSMutableDictionary<NSNumber *, _RCTTimer *> *_timers;
NSTimer *_sleepTimer;
BOOL _sendIdleEvents;
BOOL _inBackground;
}

@synthesize bridge = _bridge;
Expand All @@ -110,20 +111,21 @@ - (void)setBridge:(RCTBridge *)bridge

_paused = YES;
_timers = [NSMutableDictionary new];
_inBackground = NO;

for (NSString *name in @[UIApplicationWillResignActiveNotification,
UIApplicationDidEnterBackgroundNotification,
UIApplicationWillTerminateNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(stopTimers)
selector:@selector(appDidMoveToBackground)
name:name
object:nil];
}

for (NSString *name in @[UIApplicationDidBecomeActiveNotification,
UIApplicationWillEnterForegroundNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(startTimers)
selector:@selector(appDidMoveToForeground)
name:name
object:nil];
}
Expand All @@ -148,8 +150,29 @@ - (void)invalidate
_bridge = 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) {
Expand All @@ -160,7 +183,7 @@ - (void)stopTimers

- (void)startTimers
{
if (!_bridge || ![self hasPendingTimers]) {
if (!_bridge || _inBackground || ![self hasPendingTimers]) {
return;
}

Expand All @@ -174,19 +197,23 @@ - (void)startTimers

- (BOOL)hasPendingTimers
{
return _sendIdleEvents || _timers.count > 0;
@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
for (_RCTTimer *timer in _timers.allValues) {
if ([timer shouldFire:now]) {
[timersToCall addObject:timer];
} else {
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
@synchronized (_timers) {
for (_RCTTimer *timer in _timers.allValues) {
if ([timer shouldFire:now]) {
[timersToCall addObject:timer];
} else {
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
}
}
}

Expand All @@ -206,7 +233,9 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update
[timer reschedule];
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
} else {
[_timers removeObjectForKey:timer.callbackID];
@synchronized (_timers) {
[_timers removeObjectForKey:timer.callbackID];
}
}
}

Expand All @@ -225,10 +254,18 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update
// 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) {
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 (_timers.count == 0) {
if (timerCount == 0) {
_paused = YES;
}
// If the next timer is more than 1 second out, pause and schedule an NSTimer;
Expand All @@ -241,16 +278,18 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update

- (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];
@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];
}
}
}

Expand Down Expand Up @@ -294,8 +333,13 @@ - (void)timerDidFire
interval:jsDuration
targetTime:targetTime
repeats:repeats];
_timers[callbackID] = timer;
if (_paused) {
@synchronized (_timers) {
_timers[callbackID] = timer;
}

if (_inBackground) {
[self scheduleSleepTimer:timer.target];
} else if (_paused) {
if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) {
[self scheduleSleepTimer:timer.target];
} else {
Expand All @@ -306,7 +350,9 @@ - (void)timerDidFire

RCT_EXPORT_METHOD(deleteTimer:(nonnull NSNumber *)timerID)
{
[_timers removeObjectForKey:timerID];
@synchronized (_timers) {
[_timers removeObjectForKey:timerID];
}
if (![self hasPendingTimers]) {
[self stopTimers];
}
Expand Down