diff --git a/React/Base/RCTDisplayLink.m b/React/Base/RCTDisplayLink.m index 12450cb96cd..7042dc0b692 100644 --- a/React/Base/RCTDisplayLink.m +++ b/React/Base/RCTDisplayLink.m @@ -18,6 +18,10 @@ #import "RCTModuleData.h" #import "RCTProfile.h" +#define RCTAssertRunLoop() \ + RCTAssert(_runLoop == [NSRunLoop currentRunLoop], \ + @"This method must be called on the CADisplayLink run loop") + @implementation RCTDisplayLink { CADisplayLink *_jsDisplayLink; @@ -38,12 +42,13 @@ - (void)registerModuleForFrameUpdates:(id)module withModuleData:(RCTModuleData *)moduleData { - if ([_frameUpdateObservers containsObject:moduleData] || - ![moduleData.moduleClass conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { + if (![moduleData.moduleClass conformsToProtocol:@protocol(RCTFrameUpdateObserver)] || + [_frameUpdateObservers containsObject:moduleData]) { return; } [_frameUpdateObservers addObject:moduleData]; + // Don't access the module instance via moduleData, as this will cause deadlock id observer = (id)module; __weak typeof(self) weakSelf = self; @@ -54,16 +59,28 @@ } CFRunLoopRef cfRunLoop = [strongSelf->_runLoop getCFRunLoop]; - - if (!self->_runLoop) { + if (!cfRunLoop) { return; } - CFRunLoopPerformBlock(cfRunLoop, kCFRunLoopDefaultMode, ^{ - [weakSelf updateJSDisplayLinkState]; - }); - CFRunLoopWakeUp(cfRunLoop); + if ([NSRunLoop currentRunLoop] == strongSelf->_runLoop) { + [weakSelf updateJSDisplayLinkState]; + } else { + CFRunLoopPerformBlock(cfRunLoop, kCFRunLoopDefaultMode, ^{ + [weakSelf updateJSDisplayLinkState]; + }); + CFRunLoopWakeUp(cfRunLoop); + } }; + + // Assuming we're paused right now, we only need to update the display link's state + // when the new observer is not paused. If it not paused, the observer will immediately + // start receiving updates anyway. + if (![observer isPaused] && _runLoop) { + CFRunLoopPerformBlock([_runLoop getCFRunLoop], kCFRunLoopDefaultMode, ^{ + [self updateJSDisplayLinkState]; + }); + } } - (void)addToRunLoop:(NSRunLoop *)runLoop @@ -77,12 +94,6 @@ [_jsDisplayLink invalidate]; } -- (void)assertOnRunLoop -{ - RCTAssert(_runLoop == [NSRunLoop currentRunLoop], - @"This method must be called on the CADisplayLink run loop"); -} - - (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue { @@ -95,7 +106,7 @@ - (void)_jsThreadUpdate:(CADisplayLink *)displayLink { - [self assertOnRunLoop]; + RCTAssertRunLoop(); RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTDisplayLink _jsThreadUpdate:]", nil); @@ -121,7 +132,7 @@ - (void)updateJSDisplayLinkState { - [self assertOnRunLoop]; + RCTAssertRunLoop(); BOOL pauseDisplayLink = YES; for (RCTModuleData *moduleData in _frameUpdateObservers) { @@ -131,6 +142,7 @@ break; } } + _jsDisplayLink.paused = pauseDisplayLink; } diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index 2d4360d8948..38c18c8081b 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -426,7 +426,7 @@ SEL RCTParseMethodSignature(NSString *methodSignature, NSArray_target = target; + } + return proxy; +} + +- (void)timerDidFire +{ + [_target timerDidFire]; +} + +@end + @implementation RCTTiming { - NSMutableDictionary *_timers; + NSMutableDictionary *_timers; + NSTimer *_sleepTimer; } @synthesize bridge = _bridge; @@ -95,6 +124,7 @@ RCT_EXPORT_MODULE() - (void)dealloc { + [_sleepTimer invalidate]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -111,7 +141,12 @@ RCT_EXPORT_MODULE() - (void)stopTimers { - self.paused = YES; + if (!_paused) { + _paused = YES; + if (_pauseCallback) { + _pauseCallback(); + } + } } - (void)startTimers @@ -120,13 +155,8 @@ RCT_EXPORT_MODULE() return; } - self.paused = NO; -} - -- (void)setPaused:(BOOL)paused -{ - if (_paused != paused) { - _paused = paused; + if (_paused) { + _paused = NO; if (_pauseCallback) { _pauseCallback(); } @@ -135,23 +165,65 @@ RCT_EXPORT_MODULE() - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update { + NSDate *nextScheduledTarget = [NSDate distantFuture]; NSMutableArray *timersToCall = [NSMutableArray new]; - for (RCTTimer *timer in _timers.allValues) { + for (_RCTTimer *timer in _timers.allValues) { + NSDate *target = timer.target; if ([timer updateFoundNeedsJSUpdate]) { [timersToCall addObject:timer.callbackID]; } if (!timer.target) { [_timers removeObjectForKey:timer.callbackID]; + } else { + nextScheduledTarget = [nextScheduledTarget earlierDate:timer.target]; } } - // call timers that need to be called + // Call timers that need to be called if (timersToCall.count > 0) { [_bridge enqueueJSCall:@"JSTimersExecution.callTimers" args:@[timersToCall]]; + + // If we call at least one timer this frame, don't switch to a paused state yet, so if + // in response to this timer another timer is scheduled, we don't pause and unpause + // the displaylink frivolously. + return; } + // No need to call the pauseCallback as RCTDisplayLink will ask us about our paused + // status immediately after completing this call if (_timers.count == 0) { - [self stopTimers]; + _paused = YES; + } + // If the next timer is more than 1 second out, pause and schedule an NSTimer; + else if ([nextScheduledTarget timeIntervalSinceNow] > kMinimumSleepInterval) { + [self scheduleSleepTimer:nextScheduledTarget]; + _paused = YES; + } +} + +- (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]; + } +} + +- (void)timerDidFire +{ + _sleepTimer = nil; + if (_paused) { + [self startTimers]; + + // Immediately dispatch frame, so we don't have to wait on the displaylink. + [self didUpdateFrame:nil]; } } @@ -180,12 +252,18 @@ RCT_EXPORT_METHOD(createTimer:(nonnull NSNumber *)callbackID jsDuration = 0; } - RCTTimer *timer = [[RCTTimer alloc] initWithCallbackID:callbackID - interval:jsDuration - targetTime:targetTime - repeats:repeats]; + _RCTTimer *timer = [[_RCTTimer alloc] initWithCallbackID:callbackID + interval:jsDuration + targetTime:targetTime + repeats:repeats]; _timers[callbackID] = timer; - [self startTimers]; + if (_paused) { + if ([timer.target timeIntervalSinceNow] > kMinimumSleepInterval) { + [self scheduleSleepTimer:timer.target]; + } else { + [self startTimers]; + } + } } RCT_EXPORT_METHOD(deleteTimer:(nonnull NSNumber *)timerID)