[ReactNative] Use a single DisplayLink held by the bridge

This commit is contained in:
Tadeu Zagallo 2015-04-15 07:07:19 -07:00
parent 4f8b2825a0
commit 75e4e124fa
9 changed files with 176 additions and 59 deletions

View File

@ -10,6 +10,7 @@
#import <UIKit/UIKit.h>
#import "RCTBridgeModule.h"
#import "RCTFrameUpdate.h"
#import "RCTInvalidating.h"
#import "RCTJavaScriptExecutor.h"
@ -122,4 +123,14 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method;
*/
- (void)reload;
/**
* Add a new observer that will be called on every screen refresh
*/
- (void)addFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer;
/**
* Stop receiving screen refresh updates for the given observer
*/
- (void)removeFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer;
@end

View File

@ -677,6 +677,73 @@ static NSDictionary *RCTLocalModulesConfig()
return localModules;
}
@interface RCTDisplayLink : NSObject <RCTInvalidating>
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
@end
@interface RCTBridge (RCTDisplayLink)
- (void)_update:(CADisplayLink *)displayLink;
@end
@implementation RCTDisplayLink
{
__weak RCTBridge *_bridge;
CADisplayLink *_displayLink;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super init])) {
_bridge = bridge;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
return self;
}
- (BOOL)isValid
{
return _displayLink != nil;
}
- (void)invalidate
{
if (self.isValid) {
[_displayLink invalidate];
_displayLink = nil;
}
}
- (void)_update:(CADisplayLink *)displayLink
{
[_bridge _update:displayLink];
}
@end
@interface RCTFrameUpdate (Private)
- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink;
@end
@implementation RCTFrameUpdate
- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink
{
if ((self = [super init])) {
_timestamp = displayLink.timestamp;
_deltaTime = displayLink.duration;
}
return self;
}
@end
@implementation RCTBridge
{
RCTSparseArray *_modulesByID;
@ -685,6 +752,8 @@ static NSDictionary *RCTLocalModulesConfig()
Class _executorClass;
NSURL *_bundleURL;
RCTBridgeModuleProviderBlock _moduleProvider;
RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers;
BOOL _loading;
}
@ -711,6 +780,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
_latestJSExecutor = _javaScriptExecutor;
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
_shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL);
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
_frameUpdateObservers = [[NSMutableSet alloc] init];
// Register passed-in module instances
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
@ -891,6 +962,9 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
[_javaScriptExecutor invalidate];
_javaScriptExecutor = nil;
[_displayLink invalidate];
_frameUpdateObservers = nil;
// Invalidate modules
for (id target in _modulesByID.allObjects) {
if ([target respondsToSelector:@selector(invalidate)]) {
@ -1075,6 +1149,26 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
return YES;
}
- (void)_update:(CADisplayLink *)displayLink
{
RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
for (id<RCTFrameUpdateObserver> observer in _frameUpdateObservers) {
if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) {
[observer didUpdateFrame:frameUpdate];
}
}
}
- (void)addFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer
{
[_frameUpdateObservers addObject:observer];
}
- (void)removeFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer
{
[_frameUpdateObservers removeObject:observer];
}
- (void)reload
{
if (!_loading) {

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/**
* Interface containing the information about the last screen refresh.
*/
@interface RCTFrameUpdate : NSObject
/**
* Timestamp for the actual screen refresh
*/
@property (nonatomic, readonly) NSTimeInterval timestamp;
/**
* Time since the last frame update ( >= 16.6ms )
*/
@property (nonatomic, readonly) NSTimeInterval deltaTime;
@end
/**
* Protocol that must be implemented for subscribing to display refreshes (DisplayLink updates)
*/
@protocol RCTFrameUpdateObserver <NSObject>
/**
* Method called on every screen refresh (if paused != YES)
*/
- (void)didUpdateFrame:(RCTFrameUpdate *)update;
@optional
/**
* Synthesize and set to true to pause the calls to -[didUpdateFrame:]
*/
@property (nonatomic, assign, getter=isPaused) BOOL paused;
@end

View File

@ -10,8 +10,9 @@
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
#import "RCTFrameUpdate.h"
#import "RCTInvalidating.h"
@interface RCTTiming : NSObject <RCTBridgeModule, RCTInvalidating>
@interface RCTTiming : NSObject <RCTBridgeModule, RCTInvalidating, RCTFrameUpdateObserver>
@end

View File

@ -58,7 +58,6 @@
@implementation RCTTiming
{
RCTSparseArray *_timers;
id _updateTimer;
}
@synthesize bridge = _bridge;
@ -113,32 +112,21 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers)
- (void)stopTimers
{
[_updateTimer invalidate];
_updateTimer = nil;
[_bridge removeFrameUpdateObserver:self];
}
- (void)startTimers
{
RCTAssertMainThread();
if (![self isValid] || _updateTimer != nil || _timers.count == 0) {
if (![self isValid] || _timers.count == 0) {
return;
}
_updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
if (_updateTimer) {
[_updateTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
} else {
RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead.");
_updateTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60)
target:self
selector:@selector(update)
userInfo:nil
repeats:YES];
}
[_bridge addFrameUpdateObserver:self];
}
- (void)update
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
RCTAssertMainThread();

View File

@ -154,6 +154,7 @@
13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = "<group>"; };
14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = "<group>"; };
14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = "<group>"; };
1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.h; sourceTree = "<group>"; };
14435CE11AAC4AE100FC20F4 /* RCTMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMap.h; sourceTree = "<group>"; };
14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = "<group>"; };
14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = "<group>"; };
@ -391,6 +392,7 @@
83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */,
83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */,
83CBBA501A601E3B00E9B192 /* RCTUtils.m */,
1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */,
);
path = Base;
sourceTree = "<group>";

View File

@ -9,16 +9,17 @@
#import <UIKit/UIKit.h>
#import "RCTFrameUpdate.h"
#import "RCTInvalidating.h"
@class RCTEventDispatcher;
@class RCTBridge;
@interface RCTNavigator : UIView <RCTInvalidating>
@interface RCTNavigator : UIView <RCTFrameUpdateObserver>
@property (nonatomic, strong) UIView *reactNavSuperviewLink;
@property (nonatomic, assign) NSInteger requestedTopOfStack;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
/**
* Schedules a JavaScript navigation and prevents `UIKit` from navigating until

View File

@ -10,6 +10,7 @@
#import "RCTNavigator.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
@ -190,10 +191,6 @@ NSInteger kNeverProgressed = -10000;
@end
@interface RCTNavigator() <RCTWrapperViewControllerNavigationListener, UINavigationControllerDelegate>
{
RCTEventDispatcher *_eventDispatcher;
NSInteger _numberOfViewControllerMovesToIgnore;
}
@property (nonatomic, assign) NSInteger previousRequestedTopOfStack;
@ -251,7 +248,6 @@ NSInteger kNeverProgressed = -10000;
*
*/
@property (nonatomic, readonly, assign) CGFloat mostRecentProgress;
@property (nonatomic, readwrite, strong) CADisplayLink *displayLink;
@property (nonatomic, readonly, strong) NSTimer *runTimer;
@property (nonatomic, readonly, assign) NSInteger currentlyTransitioningFrom;
@property (nonatomic, readonly, assign) NSInteger currentlyTransitioningTo;
@ -263,22 +259,17 @@ NSInteger kNeverProgressed = -10000;
@end
@implementation RCTNavigator
{
__weak RCTBridge *_bridge;
NSInteger _numberOfViewControllerMovesToIgnore;
}
- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
- (id)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super initWithFrame:CGRectZero])) {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(reportNavigationProgress:)];
_bridge = bridge;
_mostRecentProgress = kNeverProgressed;
_dummyView = [[UIView alloc] initWithFrame:CGRectZero];
if (_displayLink) {
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
_displayLink.paused = YES;
} else {
// It's okay to leak this on a build bot.
RCTLogWarn(@"Failed to create a display link (probably on automated build system) - using an NSTimer for AppEngine instead.");
_runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(reportNavigationProgress:) userInfo:nil repeats:YES];
}
_eventDispatcher = eventDispatcher;
_previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push.
_previousViews = @[];
_currentViews = [[NSMutableArray alloc] initWithCapacity:0];
@ -295,7 +286,7 @@ NSInteger kNeverProgressed = -10000;
return self;
}
- (void)reportNavigationProgress:(CADisplayLink *)sender
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
if (_currentlyTransitioningFrom != _currentlyTransitioningTo) {
UIView *topView = _dummyView;
@ -307,7 +298,7 @@ NSInteger kNeverProgressed = -10000;
return;
}
_mostRecentProgress = nextProgress;
[_eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{
[_bridge.eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{
@"fromIndex": @(_currentlyTransitioningFrom),
@"toIndex": @(_currentlyTransitioningTo),
@"progress": @(nextProgress),
@ -350,16 +341,14 @@ NSInteger kNeverProgressed = -10000;
_dummyView.frame = (CGRect){{destination}};
_currentlyTransitioningFrom = indexOfFrom;
_currentlyTransitioningTo = indexOfTo;
if (indexOfFrom != indexOfTo) {
_displayLink.paused = NO;
}
[_bridge addFrameUpdateObserver:self];
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[weakSelf freeLock];
_currentlyTransitioningFrom = 0;
_currentlyTransitioningTo = 0;
_dummyView.frame = CGRectZero;
_displayLink.paused = YES;
[_bridge removeFrameUpdateObserver:self];
// Reset the parallel position tracker
}];
}
@ -400,19 +389,6 @@ NSInteger kNeverProgressed = -10000;
return _currentViews;
}
- (BOOL)isValid
{
return _displayLink != nil;
}
- (void)invalidate
{
// Prevent displayLink from retaining the navigator indefinitely
[_displayLink invalidate];
_displayLink = nil;
_runTimer = nil;
}
- (void)layoutSubviews
{
[super layoutSubviews];
@ -430,7 +406,7 @@ NSInteger kNeverProgressed = -10000;
- (void)handleTopOfStackChanged
{
[_eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{
[_bridge.eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{
@"target":self.reactTag,
@"stackLength":@(_navigationController.viewControllers.count)
}];
@ -438,7 +414,7 @@ NSInteger kNeverProgressed = -10000;
- (void)dispatchFakeScrollEvent
{
[_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove
[_bridge.eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove
reactTag:self.reactTag
scrollView:nil
userData:nil];
@ -511,7 +487,7 @@ NSInteger kNeverProgressed = -10000;
if (jsGettingAhead) {
if (reactPushOne) {
UIView *lastView = [_currentViews lastObject];
RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_eventDispatcher];
RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_bridge.eventDispatcher];
vc.navigationListener = self;
_numberOfViewControllerMovesToIgnore = 1;
[_navigationController pushViewController:vc animated:(currentReactCount > 1)];

View File

@ -21,7 +21,7 @@ RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RCTNavigator alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
return [[RCTNavigator alloc] initWithBridge:self.bridge];
}
RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack, NSInteger)