Skip to content
Open
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
88 changes: 77 additions & 11 deletions packages/react-native/React/CoreModules/RCTTiming.mm
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ @implementation RCTTiming {
BOOL _sendIdleEvents;
BOOL _inBackground;
id<RCTTimingDelegate> _timingDelegate;
// The run loop of the thread that creates timers (the JS thread). Every method that touches the
// timer state runs there; app lifecycle notifications, posted on the main thread, are forwarded
// to it (see -performOnTimingThread:).
CFRunLoopRef _timingRunLoop;
}

@synthesize paused = _paused;
Expand All @@ -119,11 +123,16 @@ - (void)setup
_paused = YES;
_timers = [NSMutableDictionary new];
_inBackground = NO;
__weak RCTTiming *weakSelf = self;
RCTExecuteOnMainQueue(^{
if (!self->_inBackground &&
([RCTSharedApplication() applicationState] == UIApplicationStateBackground ||
[UIDevice currentDevice].proximityState)) {
[self appDidMoveToBackground];
if ([RCTSharedApplication() applicationState] == UIApplicationStateBackground ||
[UIDevice currentDevice].proximityState) {
[weakSelf performOnTimingThread:^{
RCTTiming *strongSelf = weakSelf;
if (strongSelf && !strongSelf->_inBackground) {
[strongSelf appDidMoveToBackground];
}
}];
}
});

Expand All @@ -133,14 +142,14 @@ - (void)setup
UIApplicationWillTerminateNotification
]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidMoveToBackground)
selector:@selector(appDidMoveToBackgroundNotification)
name:name
object:nil];
}

for (NSString *name in @[ UIApplicationDidBecomeActiveNotification, UIApplicationWillEnterForegroundNotification ]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidMoveToForeground)
selector:@selector(appDidMoveToForegroundNotification)
name:name
object:nil];
}
Expand All @@ -154,6 +163,54 @@ - (void)setup
- (void)dealloc
{
[_sleepTimer invalidate];
if (_timingRunLoop) {
CFRelease(_timingRunLoop);
}
}

/**
* Runs `block` on the thread that creates timers.
*
* App lifecycle notifications are posted on the main thread, while the rest of this class runs on
* the timers thread. Handling them inline made two threads write `_paused` and `_inBackground`
* without synchronization, and installed the background NSTimer (-scheduleSleepTimer:) on the
* main run loop in NSDefaultRunLoopMode. That timer does not fire while the main thread is busy
* or in another run loop mode — as during a Face ID prompt — and the timers thread can then only
* move its fire date, so every JS timer stalls. Forwarding keeps all timer state, and the sleep
* timer, on one thread.
*
* Before the first timer is created the timers thread is unknown; there is no timer to race with
* then, so the block runs in place.
*/
- (void)performOnTimingThread:(dispatch_block_t)block
{
CFRunLoopRef runLoop = NULL;
@synchronized(self) {
if (!_timingRunLoop || _timingRunLoop == CFRunLoopGetCurrent()) {
block();
return;
}
runLoop = (CFRunLoopRef)CFRetain(_timingRunLoop);
}
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, block);
CFRunLoopWakeUp(runLoop);
CFRelease(runLoop);
}

- (void)appDidMoveToBackgroundNotification
{
__weak RCTTiming *weakSelf = self;
[self performOnTimingThread:^{
[weakSelf appDidMoveToBackground];
}];
}

- (void)appDidMoveToForegroundNotification
{
__weak RCTTiming *weakSelf = self;
[self performOnTimingThread:^{
[weakSelf appDidMoveToForeground];
}];
}

- (void)invalidate
Expand Down Expand Up @@ -182,11 +239,14 @@ - (void)appDidMoveToForeground
- (void)proximityChanged
{
BOOL isClose = [UIDevice currentDevice].proximityState;
if (isClose) {
[self appDidMoveToBackground];
} else {
[self appDidMoveToForeground];
}
__weak RCTTiming *weakSelf = self;
[self performOnTimingThread:^{
if (isClose) {
[weakSelf appDidMoveToBackground];
} else {
[weakSelf appDidMoveToForeground];
}
}];
}

- (void)stopTimers
Expand Down Expand Up @@ -329,6 +389,12 @@ - (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID
jsSchedulingTime:(NSDate *)jsSchedulingTime
repeats:(BOOL)repeats
{
@synchronized(self) {
if (!_timingRunLoop) {
_timingRunLoop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent());
}
}

NSTimeInterval jsSchedulingOverhead = MAX(-jsSchedulingTime.timeIntervalSinceNow, 0);

NSTimeInterval targetTime = jsDuration - jsSchedulingOverhead;
Expand Down
Loading