Skip to content

Instantly share code, notes, and snippets.

@cojo
Created December 11, 2018 22:10
Show Gist options
  • Save cojo/a8fa26bbb791cf40c8ca863e3a77471a to your computer and use it in GitHub Desktop.
Save cojo/a8fa26bbb791cf40c8ca863e3a77471a to your computer and use it in GitHub Desktop.
React Native Background Timers w/ synchronization fixes
From be12c321f40f58782f103a526937df3f06e773af Mon Sep 17 00:00:00 2001
From: James Reggio <[email protected]>
Date: Tue, 2 Jan 2018 12:49:05 -0500
Subject: [PATCH] Fire timers in the background exclusively via NSTimer
---
React/Modules/RCTTiming.m | 40 ++++++++++++++++++++++++++++++++++-----
1 file changed, 35 insertions(+), 5 deletions(-)
diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m
index 3a2474af806f..54c40e9065e3 100644
--- a/React/Modules/RCTTiming.m
+++ b/React/Modules/RCTTiming.m
@@ -96,6 +96,7 @@
NSMutableDictionary<NSNumber *, _RCTTimer *> *_timers;
NSTimer *_sleepTimer;
BOOL _sendIdleEvents;
+ BOOL _inBackground;
}
@synthesize bridge = _bridge;
@@ -110,12 +111,13 @@
_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];
}
@@ -123,7 +125,7 @@
for (NSString *name in @[UIApplicationDidBecomeActiveNotification,
UIApplicationWillEnterForegroundNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(startTimers)
+ selector:@selector(appDidMoveToForeground)
name:name
object:nil];
}
@@ -148,8 +150,29 @@
_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) {
@@ -160,7 +183,7 @@
- (void)startTimers
{
- if (!_bridge || ![self hasPendingTimers]) {
+ if (!_bridge || _inBackground || ![self hasPendingTimers]) {
return;
}
@@ -174,7 +197,9 @@
- (BOOL)hasPendingTimers
{
- return _sendIdleEvents || _timers.count > 0;
+ @synchronized (_timers) {
+ return _sendIdleEvents || _timers.count > 0;
+ }
}
- (void)didUpdateFrame:(RCTFrameUpdate *)update
@@ -182,11 +207,13 @@
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];
+ }
}
}
@@ -206,7 +233,9 @@
[timer reschedule];
nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target];
} else {
- [_timers removeObjectForKey:timer.callbackID];
+ @synchronized (_timers) {
+ [_timers removeObjectForKey:timer.callbackID];
+ }
}
}
@@ -225,10 +254,18 @@
// 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;
@@ -241,16 +278,18 @@
- (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];
+ }
}
}
@@ -294,8 +333,13 @@
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 {
@@ -306,7 +350,9 @@
RCT_EXPORT_METHOD(deleteTimer:(nonnull NSNumber *)timerID)
{
- [_timers removeObjectForKey:timerID];
+ @synchronized (_timers) {
+ [_timers removeObjectForKey:timerID];
+ }
if (![self hasPendingTimers]) {
[self stopTimers];
}
@@ -322,4 +368,4 @@
}
}
-@end
\ No newline at end of file
+@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment