@@ -95,6 +95,10 @@ @implementation RCTTiming {
9595 BOOL _sendIdleEvents;
9696 BOOL _inBackground;
9797 id <RCTTimingDelegate> _timingDelegate;
98+ // The run loop of the thread that creates timers (the JS thread). Every method that touches the
99+ // timer state runs there; app lifecycle notifications, posted on the main thread, are forwarded
100+ // to it (see -performOnTimingThread:).
101+ CFRunLoopRef _timingRunLoop;
98102}
99103
100104@synthesize paused = _paused;
@@ -119,11 +123,16 @@ - (void)setup
119123 _paused = YES ;
120124 _timers = [NSMutableDictionary new ];
121125 _inBackground = NO ;
126+ __weak RCTTiming *weakSelf = self;
122127 RCTExecuteOnMainQueue (^{
123- if (!self->_inBackground &&
124- ([RCTSharedApplication () applicationState ] == UIApplicationStateBackground ||
125- [UIDevice currentDevice ].proximityState )) {
126- [self appDidMoveToBackground ];
128+ if ([RCTSharedApplication () applicationState ] == UIApplicationStateBackground ||
129+ [UIDevice currentDevice ].proximityState ) {
130+ [weakSelf performOnTimingThread: ^{
131+ RCTTiming *strongSelf = weakSelf;
132+ if (strongSelf && !strongSelf->_inBackground ) {
133+ [strongSelf appDidMoveToBackground ];
134+ }
135+ }];
127136 }
128137 });
129138
@@ -133,14 +142,14 @@ - (void)setup
133142 UIApplicationWillTerminateNotification
134143 ]) {
135144 [[NSNotificationCenter defaultCenter ] addObserver: self
136- selector: @selector (appDidMoveToBackground )
145+ selector: @selector (appDidMoveToBackgroundNotification )
137146 name: name
138147 object: nil ];
139148 }
140149
141150 for (NSString *name in @[ UIApplicationDidBecomeActiveNotification, UIApplicationWillEnterForegroundNotification ]) {
142151 [[NSNotificationCenter defaultCenter ] addObserver: self
143- selector: @selector (appDidMoveToForeground )
152+ selector: @selector (appDidMoveToForegroundNotification )
144153 name: name
145154 object: nil ];
146155 }
@@ -154,6 +163,54 @@ - (void)setup
154163- (void )dealloc
155164{
156165 [_sleepTimer invalidate ];
166+ if (_timingRunLoop) {
167+ CFRelease (_timingRunLoop);
168+ }
169+ }
170+
171+ /* *
172+ * Runs `block` on the thread that creates timers.
173+ *
174+ * App lifecycle notifications are posted on the main thread, while the rest of this class runs on
175+ * the timers thread. Handling them inline made two threads write `_paused` and `_inBackground`
176+ * without synchronization, and installed the background NSTimer (-scheduleSleepTimer:) on the
177+ * main run loop in NSDefaultRunLoopMode. That timer does not fire while the main thread is busy
178+ * or in another run loop mode — as during a Face ID prompt — and the timers thread can then only
179+ * move its fire date, so every JS timer stalls. Forwarding keeps all timer state, and the sleep
180+ * timer, on one thread.
181+ *
182+ * Before the first timer is created the timers thread is unknown; there is no timer to race with
183+ * then, so the block runs in place.
184+ */
185+ - (void )performOnTimingThread : (dispatch_block_t )block
186+ {
187+ CFRunLoopRef runLoop = NULL ;
188+ @synchronized (self) {
189+ if (!_timingRunLoop || _timingRunLoop == CFRunLoopGetCurrent ()) {
190+ block ();
191+ return ;
192+ }
193+ runLoop = (CFRunLoopRef)CFRetain (_timingRunLoop);
194+ }
195+ CFRunLoopPerformBlock (runLoop, kCFRunLoopCommonModes , block);
196+ CFRunLoopWakeUp (runLoop);
197+ CFRelease (runLoop);
198+ }
199+
200+ - (void )appDidMoveToBackgroundNotification
201+ {
202+ __weak RCTTiming *weakSelf = self;
203+ [self performOnTimingThread: ^{
204+ [weakSelf appDidMoveToBackground ];
205+ }];
206+ }
207+
208+ - (void )appDidMoveToForegroundNotification
209+ {
210+ __weak RCTTiming *weakSelf = self;
211+ [self performOnTimingThread: ^{
212+ [weakSelf appDidMoveToForeground ];
213+ }];
157214}
158215
159216- (void )invalidate
@@ -182,11 +239,14 @@ - (void)appDidMoveToForeground
182239- (void )proximityChanged
183240{
184241 BOOL isClose = [UIDevice currentDevice ].proximityState ;
185- if (isClose) {
186- [self appDidMoveToBackground ];
187- } else {
188- [self appDidMoveToForeground ];
189- }
242+ __weak RCTTiming *weakSelf = self;
243+ [self performOnTimingThread: ^{
244+ if (isClose) {
245+ [weakSelf appDidMoveToBackground ];
246+ } else {
247+ [weakSelf appDidMoveToForeground ];
248+ }
249+ }];
190250}
191251
192252- (void )stopTimers
@@ -329,6 +389,12 @@ - (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID
329389 jsSchedulingTime : (NSDate *)jsSchedulingTime
330390 repeats : (BOOL )repeats
331391{
392+ @synchronized (self) {
393+ if (!_timingRunLoop) {
394+ _timingRunLoop = (CFRunLoopRef)CFRetain (CFRunLoopGetCurrent ());
395+ }
396+ }
397+
332398 NSTimeInterval jsSchedulingOverhead = MAX (-jsSchedulingTime.timeIntervalSinceNow , 0 );
333399
334400 NSTimeInterval targetTime = jsDuration - jsSchedulingOverhead;
0 commit comments