From f1fb992d4c94bdcde401b20f0b67d4e1273a726a Mon Sep 17 00:00:00 2001 From: FeliksLv Date: Mon, 9 Nov 2026 01:35:26 +0800 Subject: [PATCH 1/4] Fix iOS JS timers when the system clock changes --- .../React/CoreModules/RCTTiming.mm | 75 +++++++---- .../RNTesterPods.xcodeproj/project.pbxproj | 4 + .../RCTTimingClockChangeTests.mm | 118 ++++++++++++++++++ 3 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 packages/rn-tester/RNTesterUnitTests/RCTTimingClockChangeTests.mm diff --git a/packages/react-native/React/CoreModules/RCTTiming.mm b/packages/react-native/React/CoreModules/RCTTiming.mm index d77719a4b5d6..dde3642a13bf 100644 --- a/packages/react-native/React/CoreModules/RCTTiming.mm +++ b/packages/react-native/React/CoreModules/RCTTiming.mm @@ -7,12 +7,16 @@ #import "RCTTiming.h" +#include +#include + #import #import #import #import static const NSTimeInterval kMinimumSleepInterval = 1; +static const NSTimeInterval kNoTimerTarget = INFINITY; // 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. @@ -22,15 +26,23 @@ @interface _RCTTimer : NSObject -@property (nonatomic, strong, readonly) NSDate *target; +@property (nonatomic, assign, readonly) NSTimeInterval target; @property (nonatomic, assign, readonly) BOOL repeats; @property (nonatomic, copy, readonly) NSNumber *callbackID; @property (nonatomic, assign, readonly) NSTimeInterval interval; ++ (NSTimeInterval)monotonicTime; + @end @implementation _RCTTimer ++ (NSTimeInterval)monotonicTime +{ + // Timer delays measure elapsed time, including time spent asleep. + return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / (NSTimeInterval)NSEC_PER_SEC; +} + - (instancetype)initWithCallbackID:(NSNumber *)callbackID interval:(NSTimeInterval)interval targetTime:(NSTimeInterval)targetTime @@ -40,7 +52,7 @@ - (instancetype)initWithCallbackID:(NSNumber *)callbackID _interval = interval; _repeats = repeats; _callbackID = callbackID; - _target = [NSDate dateWithTimeIntervalSinceNow:targetTime]; + _target = [_RCTTimer monotonicTime] + targetTime; } return self; } @@ -48,18 +60,15 @@ - (instancetype)initWithCallbackID:(NSNumber *)callbackID /** * Returns `YES` if we should invoke the JS callback. */ -- (BOOL)shouldFire:(NSDate *)now +- (BOOL)shouldFire:(NSTimeInterval)now { - if (_target && [_target timeIntervalSinceDate:now] <= 0) { - return YES; - } - return NO; + return _target <= now; } - (void)reschedule { // The JS Timers will do fine grained calculating of expired timeouts. - _target = [NSDate dateWithTimeIntervalSinceNow:_interval]; + _target = [_RCTTimer monotonicTime] + _interval; } @end @@ -92,6 +101,7 @@ - (void)timerDidFire @implementation RCTTiming { NSMutableDictionary *_timers; NSTimer *_sleepTimer; + NSTimeInterval _sleepTimerTarget; BOOL _sendIdleEvents; BOOL _inBackground; id _timingDelegate; @@ -226,15 +236,15 @@ - (BOOL)hasPendingTimers - (void)didUpdateFrame:(RCTFrameUpdate *)update { - NSDate *nextScheduledTarget = [NSDate distantFuture]; + NSTimeInterval nextScheduledTarget = kNoTimerTarget; NSMutableArray<_RCTTimer *> *timersToCall = [NSMutableArray new]; - NSDate *now = [NSDate date]; // compare all the timers to the same base time + NSTimeInterval now = [_RCTTimer monotonicTime]; @synchronized(_timers) { for (_RCTTimer *timer in _timers.allValues) { if ([timer shouldFire:now]) { [timersToCall addObject:timer]; - } else { - nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target]; + } else if (timer.target < nextScheduledTarget) { + nextScheduledTarget = timer.target; } } } @@ -242,7 +252,13 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update // Call timers that need to be called if (timersToCall.count > 0) { NSArray *sortedTimers = [[timersToCall sortedArrayUsingComparator:^(_RCTTimer *a, _RCTTimer *b) { - return [a.target compare:b.target]; + if (a.target < b.target) { + return NSOrderedAscending; + } + if (a.target > b.target) { + return NSOrderedDescending; + } + return NSOrderedSame; }] valueForKey:@"callbackID"]; [_timingDelegate callTimers:sortedTimers]; } @@ -250,7 +266,9 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update for (_RCTTimer *timer in timersToCall) { if (timer.repeats) { [timer reschedule]; - nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target]; + if (timer.target < nextScheduledTarget) { + nextScheduledTarget = timer.target; + } } else { @synchronized(_timers) { [_timers removeObjectForKey:timer.callbackID]; @@ -275,7 +293,7 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update timerCount = _timers.count; } if (_inBackground) { - if (timerCount != 0u) { + if (nextScheduledTarget != kNoTimerTarget) { [self scheduleSleepTimer:nextScheduledTarget]; } } else if (!_sendIdleEvents && timersToCall.count == 0) { @@ -285,26 +303,31 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update _paused = YES; } // If the next timer is more than 1 second out, pause and schedule an NSTimer; - else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) { + else if ( + nextScheduledTarget != kNoTimerTarget && + nextScheduledTarget - [_RCTTimer monotonicTime] > kMinimumSleepInterval) { [self scheduleSleepTimer:nextScheduledTarget]; _paused = YES; } } } -- (void)scheduleSleepTimer:(NSDate *)sleepTarget +- (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget { @synchronized(self) { + NSTimeInterval delay = MAX(sleepTarget - [_RCTTimer monotonicTime], 0); if ((_sleepTimer == nil) || !_sleepTimer.valid) { - _sleepTimer = [[NSTimer alloc] initWithFireDate:sleepTarget - interval:0 - target:[_RCTTimingProxy proxyWithTarget:self] - selector:@selector(timerDidFire) - userInfo:nil - repeats:NO]; + _sleepTimer = [NSTimer timerWithTimeInterval:delay + target:[_RCTTimingProxy proxyWithTarget:self] + selector:@selector(timerDidFire) + userInfo:nil + repeats:NO]; + _sleepTimerTarget = sleepTarget; [[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode]; - } else { - _sleepTimer.fireDate = [_sleepTimer.fireDate earlierDate:sleepTarget]; + } else if (sleepTarget < _sleepTimerTarget) { + // Convert to a date only when setting the run loop's wakeup. + _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; + _sleepTimerTarget = sleepTarget; } } } @@ -347,7 +370,7 @@ - (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID if (_inBackground) { [self scheduleSleepTimer:timer.target]; } else if (_paused) { - if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) { + if (timer.target - [_RCTTimer monotonicTime] > kMinimumSleepInterval) { [self scheduleSleepTimer:timer.target]; } else { [self startTimers]; diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index adc9a277d087..aa320ff89753 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */; }; 79B29C2E2E607A99007612A5 /* SceneDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 79B29C2D2E607A99007612A5 /* SceneDelegate.mm */; }; 7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */; }; + A8717727467541429C262C63 /* RCTTimingClockChangeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = A8717727467541429C262C64 /* RCTTimingClockChangeTests.mm */; }; 8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */; }; 832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; }; A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; }; @@ -89,6 +90,7 @@ 51BC9297B6C3163C14532020 /* Pods-RNTester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.release.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.release.xcconfig"; sourceTree = ""; }; 5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = ""; }; 67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTStatusBarAppearanceTests.mm; sourceTree = ""; }; + A8717727467541429C262C64 /* RCTTimingClockChangeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTimingClockChangeTests.mm; sourceTree = ""; }; 79B29C2C2E607A99007612A5 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SceneDelegate.h; path = RNTester/SceneDelegate.h; sourceTree = ""; }; 79B29C2D2E607A99007612A5 /* SceneDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SceneDelegate.mm; path = RNTester/SceneDelegate.mm; sourceTree = ""; }; 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNTester/LaunchScreen.storyboard; sourceTree = ""; }; @@ -318,6 +320,7 @@ D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */, E7DB20B322B2BAA4005AC45F /* RNTesterUnitTestsBundle.js */, 67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */, + A8717727467541429C262C64 /* RCTTimingClockChangeTests.mm */, ); path = RNTesterUnitTests; sourceTree = ""; @@ -637,6 +640,7 @@ E7DB20D822B2BAA6005AC45F /* RCTJSONTests.m in Sources */, 602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */, 7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */, + A8717727467541429C262C63 /* RCTTimingClockChangeTests.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTimingClockChangeTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTimingClockChangeTests.mm new file mode 100644 index 000000000000..763ef73a0a44 --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTTimingClockChangeTests.mm @@ -0,0 +1,118 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#import + +static constexpr NSTimeInterval kClockJump = 60 * 60; +static constexpr NSTimeInterval kTimerDelay = 0.05; +static thread_local NSTimeInterval wallClockOffset; + +static Method dateMethod() +{ + return class_getClassMethod(NSDate.class, @selector(date)); +} + +static auto originalDateImplementation() +{ + static const auto implementation = method_getImplementation(dateMethod()); + return implementation; +} + +static NSDate *dateWithClockOffset(id receiver, SEL selector) +{ + auto originalDate = reinterpret_cast(originalDateImplementation()); + NSDate *now = originalDate(receiver, selector); + return wallClockOffset == 0 ? now : [now dateByAddingTimeInterval:wallClockOffset]; +} + +@interface RCTTimingClockChangeTests : XCTestCase +@end + +@implementation RCTTimingClockChangeTests { + RCTTiming *_timing; + NSMutableArray *_callbacks; +} + +- (void)setUp +{ + [super setUp]; + _callbacks = [NSMutableArray new]; + _timing = [[RCTTiming alloc] initWithDelegate:self]; +} + +- (void)tearDown +{ + [_timing invalidate]; + _timing = nil; + [super tearDown]; +} + +- (void)callTimers:(NSArray *)timers +{ + [_callbacks addObjectsFromArray:timers]; +} + +- (void)immediatelyCallTimer:(NSNumber *)callbackID +{ + [_callbacks addObject:callbackID]; +} + +- (void)callIdleCallbacks:(NSNumber *)absoluteFrameStartMS +{ +} + +- (void)updateFrameWithClockOffset:(NSTimeInterval)offset +{ + // Simulate a clock change after scheduling. Only this thread sees the + // shifted time, and only while checking which timers are due. + IMP original = originalDateImplementation(); + wallClockOffset = offset; + method_setImplementation(dateMethod(), reinterpret_cast(dateWithClockOffset)); + @try { + [_timing didUpdateFrame:nil]; + } @finally { + method_setImplementation(dateMethod(), original); + wallClockOffset = 0; + } +} + +- (void)testClockMovingForwardDoesNotFirePendingTimeoutEarly +{ + [_timing createTimerForNextFrame:@1 duration:60 jsSchedulingTime:nil repeats:NO]; + + [self updateFrameWithClockOffset:kClockJump]; + + XCTAssertEqual(_callbacks.count, 0u); +} + +- (void)testClockMovingBackwardDoesNotDelayDueTimeout +{ + [_timing createTimerForNextFrame:@1 duration:kTimerDelay jsSchedulingTime:nil repeats:NO]; + [NSThread sleepForTimeInterval:2 * kTimerDelay]; + + [self updateFrameWithClockOffset:-kClockJump]; + + XCTAssertEqualObjects(_callbacks, (@[ @1 ])); +} + +- (void)testClockMovingBackwardDoesNotDelayRescheduledInterval +{ + [_timing createTimerForNextFrame:@1 duration:kTimerDelay jsSchedulingTime:nil repeats:YES]; + [NSThread sleepForTimeInterval:2 * kTimerDelay]; + [_timing didUpdateFrame:nil]; + XCTAssertEqualObjects(_callbacks, (@[ @1 ])); + + [NSThread sleepForTimeInterval:2 * kTimerDelay]; + [self updateFrameWithClockOffset:-kClockJump]; + + XCTAssertEqualObjects(_callbacks, (@[ @1, @1 ])); +} + +@end From 752da883ed3b72cec4eb8902d75708a18d6e0315 Mon Sep 17 00:00:00 2001 From: FeliksLv Date: Thu, 12 Nov 2026 00:40:41 +0800 Subject: [PATCH 2/4] Use steady_clock for iOS timer deadlines --- .../React/CoreModules/RCTTiming.mm | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTTiming.mm b/packages/react-native/React/CoreModules/RCTTiming.mm index dde3642a13bf..a4e1b8279b2d 100644 --- a/packages/react-native/React/CoreModules/RCTTiming.mm +++ b/packages/react-native/React/CoreModules/RCTTiming.mm @@ -8,7 +8,7 @@ #import "RCTTiming.h" #include -#include +#include #import #import @@ -24,6 +24,12 @@ // The minimum time left in a frame to trigger the idle callback. static const NSTimeInterval kIdleCallbackFrameDeadline = 0.001; +static NSTimeInterval RCTMonotonicTime() +{ + // Apple libc++ uses CLOCK_MONOTONIC_RAW, which includes time spent asleep. + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + @interface _RCTTimer : NSObject @property (nonatomic, assign, readonly) NSTimeInterval target; @@ -31,18 +37,10 @@ @interface _RCTTimer : NSObject @property (nonatomic, copy, readonly) NSNumber *callbackID; @property (nonatomic, assign, readonly) NSTimeInterval interval; -+ (NSTimeInterval)monotonicTime; - @end @implementation _RCTTimer -+ (NSTimeInterval)monotonicTime -{ - // Timer delays measure elapsed time, including time spent asleep. - return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / (NSTimeInterval)NSEC_PER_SEC; -} - - (instancetype)initWithCallbackID:(NSNumber *)callbackID interval:(NSTimeInterval)interval targetTime:(NSTimeInterval)targetTime @@ -52,7 +50,7 @@ - (instancetype)initWithCallbackID:(NSNumber *)callbackID _interval = interval; _repeats = repeats; _callbackID = callbackID; - _target = [_RCTTimer monotonicTime] + targetTime; + _target = RCTMonotonicTime() + targetTime; } return self; } @@ -68,7 +66,7 @@ - (BOOL)shouldFire:(NSTimeInterval)now - (void)reschedule { // The JS Timers will do fine grained calculating of expired timeouts. - _target = [_RCTTimer monotonicTime] + _interval; + _target = RCTMonotonicTime() + _interval; } @end @@ -238,7 +236,7 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update { NSTimeInterval nextScheduledTarget = kNoTimerTarget; NSMutableArray<_RCTTimer *> *timersToCall = [NSMutableArray new]; - NSTimeInterval now = [_RCTTimer monotonicTime]; + NSTimeInterval now = RCTMonotonicTime(); @synchronized(_timers) { for (_RCTTimer *timer in _timers.allValues) { if ([timer shouldFire:now]) { @@ -304,8 +302,7 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update } // If the next timer is more than 1 second out, pause and schedule an NSTimer; else if ( - nextScheduledTarget != kNoTimerTarget && - nextScheduledTarget - [_RCTTimer monotonicTime] > kMinimumSleepInterval) { + nextScheduledTarget != kNoTimerTarget && nextScheduledTarget - RCTMonotonicTime() > kMinimumSleepInterval) { [self scheduleSleepTimer:nextScheduledTarget]; _paused = YES; } @@ -315,7 +312,7 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update - (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget { @synchronized(self) { - NSTimeInterval delay = MAX(sleepTarget - [_RCTTimer monotonicTime], 0); + NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); if ((_sleepTimer == nil) || !_sleepTimer.valid) { _sleepTimer = [NSTimer timerWithTimeInterval:delay target:[_RCTTimingProxy proxyWithTarget:self] @@ -370,7 +367,7 @@ - (void)createTimerForNextFrame:(nonnull NSNumber *)callbackID if (_inBackground) { [self scheduleSleepTimer:timer.target]; } else if (_paused) { - if (timer.target - [_RCTTimer monotonicTime] > kMinimumSleepInterval) { + if (timer.target - RCTMonotonicTime() > kMinimumSleepInterval) { [self scheduleSleepTimer:timer.target]; } else { [self startTimers]; From e893b83d5a12999a2a41513ddf0e6d67ef29fb2c Mon Sep 17 00:00:00 2001 From: FeliksLv Date: Thu, 12 Nov 2026 02:48:52 +0800 Subject: [PATCH 3/4] Rearm iOS timers when returning to the foreground --- .../React/CoreModules/RCTTiming.mm | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTTiming.mm b/packages/react-native/React/CoreModules/RCTTiming.mm index a4e1b8279b2d..ccc4cdff9d36 100644 --- a/packages/react-native/React/CoreModules/RCTTiming.mm +++ b/packages/react-native/React/CoreModules/RCTTiming.mm @@ -183,6 +183,14 @@ - (void)appDidMoveToBackground - (void)appDidMoveToForeground { + if (_inBackground) { + @synchronized(self) { + if (_sleepTimer.valid) { + // The run-loop timer can still be waiting on time that elapsed during device sleep. + [self updateSleepTimerFireDate:_sleepTimerTarget]; + } + } + } _inBackground = NO; [self startTimers]; } @@ -309,11 +317,19 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update } } +// Call with the sleep timer lock held. +- (void)updateSleepTimerFireDate:(NSTimeInterval)sleepTarget +{ + NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); + _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; + _sleepTimerTarget = sleepTarget; +} + - (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget { @synchronized(self) { - NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); if ((_sleepTimer == nil) || !_sleepTimer.valid) { + NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); _sleepTimer = [NSTimer timerWithTimeInterval:delay target:[_RCTTimingProxy proxyWithTarget:self] selector:@selector(timerDidFire) @@ -322,9 +338,7 @@ - (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget _sleepTimerTarget = sleepTarget; [[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode]; } else if (sleepTarget < _sleepTimerTarget) { - // Convert to a date only when setting the run loop's wakeup. - _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; - _sleepTimerTarget = sleepTarget; + [self updateSleepTimerFireDate:sleepTarget]; } } } From b5bffc1f773b683ea79c7c5f7185d295f3830833 Mon Sep 17 00:00:00 2001 From: FeliksLv Date: Thu, 12 Nov 2026 03:02:37 +0800 Subject: [PATCH 4/4] Rearm iOS sleep timers for unchanged deadlines --- .../React/CoreModules/RCTTiming.mm | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTTiming.mm b/packages/react-native/React/CoreModules/RCTTiming.mm index ccc4cdff9d36..1957b54280bf 100644 --- a/packages/react-native/React/CoreModules/RCTTiming.mm +++ b/packages/react-native/React/CoreModules/RCTTiming.mm @@ -183,14 +183,6 @@ - (void)appDidMoveToBackground - (void)appDidMoveToForeground { - if (_inBackground) { - @synchronized(self) { - if (_sleepTimer.valid) { - // The run-loop timer can still be waiting on time that elapsed during device sleep. - [self updateSleepTimerFireDate:_sleepTimerTarget]; - } - } - } _inBackground = NO; [self startTimers]; } @@ -317,19 +309,11 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update } } -// Call with the sleep timer lock held. -- (void)updateSleepTimerFireDate:(NSTimeInterval)sleepTarget -{ - NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); - _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; - _sleepTimerTarget = sleepTarget; -} - - (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget { @synchronized(self) { + NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); if ((_sleepTimer == nil) || !_sleepTimer.valid) { - NSTimeInterval delay = MAX(sleepTarget - RCTMonotonicTime(), 0); _sleepTimer = [NSTimer timerWithTimeInterval:delay target:[_RCTTimingProxy proxyWithTarget:self] selector:@selector(timerDidFire) @@ -337,8 +321,10 @@ - (void)scheduleSleepTimer:(NSTimeInterval)sleepTarget repeats:NO]; _sleepTimerTarget = sleepTarget; [[NSRunLoop currentRunLoop] addTimer:_sleepTimer forMode:NSDefaultRunLoopMode]; - } else if (sleepTarget < _sleepTimerTarget) { - [self updateSleepTimerFireDate:sleepTarget]; + } else if (sleepTarget <= _sleepTimerTarget) { + // Convert to a date only when setting the run loop's wakeup. + _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; + _sleepTimerTarget = sleepTarget; } } }