diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 75a811831..0aa148fbc 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -17,6 +17,12 @@ #define TIMEOUT_SECONDS 240 +@interface RCTBridge (RCTTestRunner) + +@property (nonatomic, weak) RCTBridge *batchedBridge; + +@end + @implementation RCTTestRunner { FBSnapshotTestController *_testController; @@ -66,7 +72,7 @@ rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); - RCTTestModule *testModule = rootView.bridge.modules[testModuleName]; + RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName]; testModule.controller = _testController; testModule.testSelector = test; testModule.view = rootView; @@ -83,8 +89,6 @@ error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview]; - [rootView.bridge invalidate]; - [rootView invalidate]; RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 97658c235..f15dd70e0 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -322,23 +322,24 @@ var MessageQueueMixin = { processBatch: function(batch) { var self = this; - ReactUpdates.batchedUpdates(function() { - batch.forEach(function(call) { - invariant( - call.module === 'BatchedBridge', - 'All the calls should pass through the BatchedBridge module' - ); - if (call.method === 'callFunctionReturnFlushedQueue') { - self.callFunction.apply(self, call.args); - } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { - self.invokeCallback.apply(self, call.args); - } else { - throw new Error( - 'Unrecognized method called on BatchedBridge: ' + call.method); - } + return guardReturn(function () { + ReactUpdates.batchedUpdates(function() { + batch.forEach(function(call) { + invariant( + call.module === 'BatchedBridge', + 'All the calls should pass through the BatchedBridge module' + ); + if (call.method === 'callFunctionReturnFlushedQueue') { + self._callFunction.apply(self, call.args); + } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { + self._invokeCallback.apply(self, call.args); + } else { + throw new Error( + 'Unrecognized method called on BatchedBridge: ' + call.method); + } + }); }); - }); - return this.flushedQueue(); + }, null, this._flushedQueueUnguarded, this); }, setLoggingEnabled: function(enabled) { diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 6dfbe2414..77cbb215e 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -80,15 +80,6 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); __attribute__((used, section("__DATA,RCTImport"))) \ static const char *__rct_import_##module##_##method##__ = #module"."#method; -/** - * This method is used to execute a new application script. It is called - * internally whenever a JS application bundle is loaded/reloaded, but should - * probably not be used at any other time. - */ -- (void)enqueueApplicationScript:(NSString *)script - url:(NSURL *)url - onComplete:(RCTJavaScriptCompleteBlock)onComplete; - /** * URL of the script that was loaded into the bridge. */ @@ -122,14 +113,4 @@ 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)observer; - -/** - * Stop receiving screen refresh updates for the given observer. - */ -- (void)removeFrameUpdateObserver:(id)observer; - @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 22fce74fc..349cc725e 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -25,6 +25,7 @@ #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" +#import "RCTSourceCode.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -45,12 +46,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -/** - * Temporarily allow to turn on and off the call batching in case someone wants - * to profile both - */ -#define BATCHED_BRIDGE 0 - #ifdef __LP64__ typedef uint64_t RCTHeaderValue; typedef struct section_64 RCTHeaderSection; @@ -61,6 +56,11 @@ typedef struct section RCTHeaderSection; #define RCTGetSectByNameFromHeader getsectbynamefromheader #endif +#define RCTAssertJSThread() \ + RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTContextExecutor"] || \ + [[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \ + @"This method must be called on JS thread") + NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; @@ -122,6 +122,7 @@ static NSArray *RCTJSMethods(void) * RTCBridgeModule protocol to ensure they've been exported. This scanning * functionality is disabled in release mode to improve startup performance. */ +static NSDictionary *RCTModuleIDsByName; static NSArray *RCTModuleNamesByID; static NSArray *RCTModuleClassesByID; static NSArray *RCTBridgeModuleClassesByModuleID(void) @@ -129,8 +130,9 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - RCTModuleNamesByID = [NSMutableArray array]; - RCTModuleClassesByID = [NSMutableArray array]; + RCTModuleIDsByName = [[NSMutableDictionary alloc] init]; + RCTModuleNamesByID = [[NSMutableArray alloc] init]; + RCTModuleClassesByID = [[NSMutableArray alloc] init]; Dl_info info; dladdr(&RCTBridgeModuleClassesByModuleID, &info); @@ -162,7 +164,9 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) NSStringFromClass(cls)); // Register module - [(NSMutableArray *)RCTModuleNamesByID addObject:RCTBridgeModuleNameForClass(cls)]; + NSString *moduleName = RCTBridgeModuleNameForClass(cls); + ((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count); + [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; [(NSMutableArray *)RCTModuleClassesByID addObject:cls]; } } @@ -199,20 +203,31 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) return RCTModuleClassesByID; } +@class RCTBatchedBridge; + @interface RCTBridge () +@property (nonatomic, strong) RCTBatchedBridge *batchedBridge; +@property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; +@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher; + - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context; -#if BATCHED_BRIDGE +@end + +@interface RCTBatchedBridge : RCTBridge + +@property (nonatomic, weak) RCTBridge *parentBridge; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context; -#endif @end @@ -238,8 +253,6 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) dispatch_block_t _methodQueue; } -static Class _globalExecutorClass; - static NSString *RCTStringUpToFirstArgument(NSString *methodName) { NSRange colonRange = [methodName rangeOfString:@":"]; @@ -702,6 +715,7 @@ static NSDictionary *RCTLocalModulesConfig() @"methods": [[NSMutableDictionary alloc] init] }; localModules[moduleName] = module; + [RCTLocalModuleNames addObject:moduleName]; } // Add method if it doesn't already exist @@ -712,72 +726,18 @@ static NSDictionary *RCTLocalModulesConfig() @"methodID": @(methods.count), @"type": @"local" }; + [RCTLocalMethodNames addObject:methodName]; } // Add module and method lookup RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"]; RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"]; - [RCTLocalModuleNames addObject:moduleName]; - [RCTLocalMethodNames addObject:methodName]; } }); return localModules; } -@interface RCTDisplayLink : NSObject - -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector NS_DESIGNATED_INITIALIZER; - -@end - -@interface RCTBridge (RCTDisplayLink) - -- (void)_update:(CADisplayLink *)displayLink; - -@end - -@implementation RCTDisplayLink -{ - __weak RCTBridge *_bridge; - CADisplayLink *_displayLink; - SEL _selector; -} - -- (instancetype)initWithBridge:(RCTBridge *)bridge selector:(SEL)selector -{ - if ((self = [super init])) { - _bridge = bridge; - _selector = selector; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } - return self; -} - -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - if (self.isValid) { - [_displayLink invalidate]; - _displayLink = nil; - } -} - -- (void)_update:(CADisplayLink *)displayLink -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [_bridge performSelector:_selector withObject:displayLink]; -#pragma clang diagnostic pop -} - -@end - @interface RCTFrameUpdate (Private) - (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; @@ -798,22 +758,6 @@ static NSDictionary *RCTLocalModulesConfig() @end @implementation RCTBridge -{ - RCTSparseArray *_modulesByID; - RCTSparseArray *_queuesByID; - dispatch_queue_t _methodQueue; - NSDictionary *_modulesByName; - id _javaScriptExecutor; - Class _executorClass; - NSURL *_bundleURL; - RCTBridgeModuleProviderBlock _moduleProvider; - RCTDisplayLink *_displayLink; - RCTDisplayLink *_vsyncDisplayLink; - NSMutableSet *_frameUpdateObservers; - NSMutableArray *_scheduledCalls; - RCTSparseArray *_scheduledCallbacks; - BOOL _loading; -} static id _latestJSExecutor; @@ -821,36 +765,226 @@ static id _latestJSExecutor; moduleProvider:(RCTBridgeModuleProviderBlock)block launchOptions:(NSDictionary *)launchOptions { + RCTAssertMainThread(); + if ((self = [super init])) { + /** + * Pre register modules + */ + RCTLocalModulesConfig(); + _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; - - [self setUp]; [self bindKeys]; + [self setUp]; } return self; } +- (void)dealloc +{ + /** + * This runs only on the main thread, but crashes the subclass + * RCTAssertMainThread(); + */ + [self invalidate]; +} + +- (void)bindKeys +{ + RCTAssertMainThread(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reload) + name:RCTReloadNotification + object:nil]; + +#if TARGET_IPHONE_SIMULATOR + + __weak RCTBridge *weakSelf = self; + RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; + + // reload in current mode + [commands registerKeyCommandWithInput:@"r" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [weakSelf reload]; + }]; + +#endif + +} + +- (void)reload +{ +/** + * AnyThread + */ + dispatch_async(dispatch_get_main_queue(), ^{ + [self invalidate]; + [self setUp]; + }); +} + - (void)setUp { - Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; - _javaScriptExecutor = RCTCreateExecutor(executorClass); - _latestJSExecutor = _javaScriptExecutor; - _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); - _frameUpdateObservers = [[NSMutableSet alloc] init]; - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; + RCTAssertMainThread(); - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - _displayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_jsThreadUpdate:)]; - }]; - _vsyncDisplayLink = [[RCTDisplayLink alloc] initWithBridge:self selector:@selector(_mainThreadUpdate:)]; + _batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; +} + +- (BOOL)isValid +{ + return _batchedBridge.isValid; +} + +- (void)invalidate +{ + RCTAssertMainThread(); + + [_batchedBridge invalidate]; + _batchedBridge = nil; +} + ++ (void)logMessage:(NSString *)message level:(NSString *)level +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_latestJSExecutor.isValid) { + return; + } + + [_latestJSExecutor executeJSCall:@"RCTLog" + method:@"logIfNoNativeHook" + arguments:@[level, message] + context:RCTGetExecutorID(_latestJSExecutor) + callback:^(id json, NSError *error) {}]; + }); +} + +- (NSDictionary *)modules +{ + return _batchedBridge.modules; +} + +#define RCT_BRIDGE_WARN(...) \ +- (void)__VA_ARGS__ \ +{ \ + RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \ + only be called from bridge instance in a bridge module", @(__func__)); \ +} + +RCT_BRIDGE_WARN(enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args) +RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context) + +@end + +@implementation RCTBatchedBridge +{ + BOOL _loading; + id _javaScriptExecutor; + RCTSparseArray *_modulesByID; + RCTSparseArray *_queuesByID; + dispatch_queue_t _methodQueue; + NSDictionary *_modulesByName; + CADisplayLink *_mainDisplayLink; + CADisplayLink *_jsDisplayLink; + NSMutableSet *_frameUpdateObservers; + NSMutableArray *_scheduledCalls; + RCTSparseArray *_scheduledCallbacks; +} + +@synthesize valid = _valid; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge +{ + if (self = [super init]) { + RCTAssertMainThread(); + + _parentBridge = bridge; + + /** + * Set Initial State + */ + _valid = YES; + _loading = YES; + _frameUpdateObservers = [[NSMutableSet alloc] init]; + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _queuesByID = [[RCTSparseArray alloc] init]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + + /** + * Initialize executor to allow enqueueing calls + */ + Class executorClass = self.executorClass ?: [RCTContextExecutor class]; + _javaScriptExecutor = RCTCreateExecutor(executorClass); + _latestJSExecutor = _javaScriptExecutor; + + /** + * Setup event dispatcher before initializing modules to allow init calls + */ + self.eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; + + /** + * Initialize and register bridge modules *before* adding the display link + * so we don't have threading issues + */ + _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); + [self registerModules]; + + /** + * Start the application script + */ + [self initJS]; + } + return self; +} + +- (NSDictionary *)launchOptions +{ + return _parentBridge.launchOptions; +} + +/** + * Override to ensure that we won't create another nested bridge + */ +- (void)setUp {} + +- (void)reload +{ + [_parentBridge reload]; +} + +- (Class)executorClass +{ + return _parentBridge.executorClass; +} + +- (void)setExecutorClass:(Class)executorClass +{ + RCTAssertMainThread(); + + _parentBridge.executorClass = executorClass; +} + +- (BOOL)isLoading +{ + return _loading; +} + +- (BOOL)isValid +{ + return _valid; +} + +- (void)registerModules +{ + RCTAssertMainThread(); // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; - for (id module in _moduleProvider ? _moduleProvider() : nil) { + for (id module in _parentBridge.moduleProvider ? _parentBridge.moduleProvider() : nil) { preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; } @@ -897,7 +1031,6 @@ static id _latestJSExecutor; } // Get method queues - _queuesByID = [[RCTSparseArray alloc] init]; [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) { if ([module respondsToSelector:@selector(methodQueue)]) { dispatch_queue_t queue = [module methodQueue]; @@ -907,7 +1040,16 @@ static id _latestJSExecutor; _queuesByID[moduleID] = [NSNull null]; } } + + if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { + [_frameUpdateObservers addObject:module]; + } }]; +} + +- (void)initJS +{ + RCTAssertMainThread(); // Inject module data into JS context NSString *configJSON = RCTJSONStringify(@{ @@ -920,7 +1062,9 @@ static id _latestJSExecutor; dispatch_semaphore_signal(semaphore); }]; - _loading = YES; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); + + NSURL *bundleURL = _parentBridge.bundleURL; if (_javaScriptExecutor == nil) { /** @@ -929,11 +1073,17 @@ static id _latestJSExecutor; */ _loading = NO; - } else if (_bundleURL) { // Allow testing without a script + } else if (bundleURL) { // Allow testing without a script RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; - [loader loadBundleAtURL:_bundleURL onComplete:^(NSError *error) { + [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { _loading = NO; + if (!self.isValid) { + return; + } + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = bundleURL; + sourceCodeModule.scriptText = script; if (error != nil) { NSArray *stack = [[error userInfo] objectForKey:@"stack"]; @@ -946,37 +1096,25 @@ static id _latestJSExecutor; } } else { + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:self]; + if (!loadError) { + /** + * Register the display link to start sending js calls after everything + * is setup + */ + [_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + } + }]; } - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reload) - name:RCTReloadNotification - object:nil]; }]; } } -- (void)bindKeys -{ - -#if TARGET_IPHONE_SIMULATOR - - __weak RCTBridge *weakSelf = self; - RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - - // reload in current mode - [commands registerKeyCommandWithInput:@"r" - modifierFlags:UIKeyModifierCommand - action:^(UIKeyCommand *command) { - [weakSelf reload]; - }]; - -#endif - -} - - (NSDictionary *)modules { RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " @@ -985,53 +1123,48 @@ static id _latestJSExecutor; return _modulesByName; } -- (void)dealloc -{ - [self invalidate]; -} - #pragma mark - RCTInvalidating -- (BOOL)isValid -{ - return _javaScriptExecutor != nil; -} - - (void)invalidate { - if (!self.isValid && _modulesByID == nil) { + if (!self.isValid) { return; } - if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES]; - return; - } + RCTAssertMainThread(); - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - // Release executor + _valid = NO; if (_latestJSExecutor == _javaScriptExecutor) { _latestJSExecutor = nil; } - [_javaScriptExecutor invalidate]; - _javaScriptExecutor = nil; - [_displayLink invalidate]; - [_vsyncDisplayLink invalidate]; - _frameUpdateObservers = nil; + /** + * Main Thread deallocations + */ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_mainDisplayLink invalidate]; - // Invalidate modules - for (id target in _modulesByID.allObjects) { - if ([target respondsToSelector:@selector(invalidate)]) { - [(id)target invalidate]; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + [_jsDisplayLink invalidate]; + + // Invalidate modules + for (id target in _modulesByID.allObjects) { + if ([target respondsToSelector:@selector(invalidate)]) { + [(id)target invalidate]; + } } - } - // Release modules (breaks retain cycle if module has strong bridge reference) - _modulesByID = nil; - _queuesByID = nil; - _modulesByName = nil; + // Release modules (breaks retain cycle if module has strong bridge reference) + _javaScriptExecutor = nil; + _frameUpdateObservers = nil; + _modulesByID = nil; + _queuesByID = nil; + _modulesByName = nil; + }]; } /** @@ -1066,6 +1199,8 @@ static id _latestJSExecutor; */ - (void)_immediatelyCallTimer:(NSNumber *)timer { + RCTAssertJSThread(); + NSString *moduleDotMethod = @"RCTJSTimers.callTimers"; NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID != nil, @"Module '%@' not registered.", @@ -1074,35 +1209,29 @@ static id _latestJSExecutor; NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { -#if BATCHED_BRIDGE - dispatch_block_t block = ^{ - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - }; - if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { - [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; - } else { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; -#else - - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; -#endif + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } } - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); + RCTProfileBeginEvent(); + [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { + RCTAssertJSThread(); + RCTProfileEndEvent(@"ApplicationScript", @"js_call,init", scriptLoadError); if (scriptLoadError) { onComplete(scriptLoadError); @@ -1132,7 +1261,13 @@ static id _latestJSExecutor; - (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID { - id queue = _queuesByID[moduleID]; + RCTAssertJSThread(); + + id queue = nil; + if (moduleID) { + queue = _queuesByID[moduleID]; + } + if (queue == [NSNull null]) { [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; } else { @@ -1146,13 +1281,15 @@ static id _latestJSExecutor; */ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { -#if BATCHED_BRIDGE + /** + * AnyThread + */ - __weak RCTBridge *weakSelf = self; + __weak RCTBatchedBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileBeginEvent(); - RCTBridge *strongSelf = weakSelf; + RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { return; } @@ -1190,7 +1327,7 @@ static id _latestJSExecutor; */ if ( [args[2][0] isEqual:callArgs[2][0]] && - ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES) + (![moduleName isEqualToString:@"RCTEventEmitter"] || [args[2][1] isEqual:callArgs[2][1]]) ) { [strongSelf->_scheduledCalls removeObject:call]; } @@ -1218,10 +1355,14 @@ static id _latestJSExecutor; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { -#endif + RCTAssertJSThread(); + [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { + if (!self.isValid) { + return; + } [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; [self _handleBuffer:json context:context]; }; @@ -1237,6 +1378,8 @@ static id _latestJSExecutor; - (void)_handleBuffer:(id)buffer context:(NSNumber *)context { + RCTAssertJSThread(); + if (buffer == nil || buffer == (id)kCFNull) { return; } @@ -1307,6 +1450,11 @@ static id _latestJSExecutor; params:(NSArray *)params context:(NSNumber *)context { + RCTAssertJSThread(); + + if (!self.isValid) { + return NO; + } if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) { RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); @@ -1330,10 +1478,10 @@ static id _latestJSExecutor; return NO; } - __weak RCTBridge *weakSelf = self; + __weak RCTBatchedBridge *weakSelf = self; [self dispatchBlock:^{ RCTProfileBeginEvent(); - __strong RCTBridge *strongSelf = weakSelf; + RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid) { // strongSelf has been invalidated since the dispatch_async call and this @@ -1367,18 +1515,21 @@ static id _latestJSExecutor; - (void)_jsThreadUpdate:(CADisplayLink *)displayLink { + RCTAssertJSThread(); + RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + RCTProfileBeginEvent(); RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (id observer in _frameUpdateObservers) { if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { - [observer didUpdateFrame:frameUpdate]; + [self dispatchBlock:^{ + [observer didUpdateFrame:frameUpdate]; + } forModule:RCTModuleIDsByName[RCTBridgeModuleNameForClass([observer class])]]; } } -#if BATCHED_BRIDGE - NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) { @@ -1393,67 +1544,41 @@ static id _latestJSExecutor; context:RCTGetExecutorID(_javaScriptExecutor)]; } -#endif - RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink { + RCTAssertMainThread(); + RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); } -- (void)addFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers addObject:observer]; -} - -- (void)removeFrameUpdateObserver:(id)observer -{ - [_frameUpdateObservers removeObject:observer]; -} - -- (void)reload -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (!_loading) { - // If the bridge has not loaded yet, the context will be already invalid at - // the time the javascript gets executed. - // It will crash the javascript, and even the next `load` won't render. - [self invalidate]; - [self setUp]; - } - }); -} - -+ (void)logMessage:(NSString *)message level:(NSString *)level -{ - if (![_latestJSExecutor isValid]) { - return; - } - - // Note: the js executor could get invalidated while we're trying to call - // this...need to watch out for that. - [_latestJSExecutor executeJSCall:@"RCTLog" - method:@"logIfNoNativeHook" - arguments:@[level, message] - context:RCTGetExecutorID(_latestJSExecutor) - callback:^(id json, NSError *error) {}]; -} - - (void)startProfiling { - if (![_bundleURL.scheme isEqualToString:@"http"]) { + RCTAssertMainThread(); + + if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { RCTLogError(@"To run the profiler you must be running from the dev server"); return; } + + [_mainDisplayLink invalidate]; + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + RCTProfileInit(); } - (void)stopProfiling { + RCTAssertMainThread(); + + [_mainDisplayLink invalidate]; + NSString *log = RCTProfileEnd(); - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port]; + NSURL *bundleURL = _parentBridge.bundleURL; + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; NSURL *URL = [NSURL URLWithString:URLString]; NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; URLRequest.HTTPMethod = @"POST"; diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index c6063caa2..6c64e4677 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -123,10 +123,12 @@ RCT_EXPORT_MODULE() { _settings = [NSMutableDictionary dictionaryWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]]; - self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; - self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; - self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; - self.executorClass = NSClassFromString(_settings[@"executorClass"]); + dispatch_async(dispatch_get_main_queue(), ^{ + self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; + self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; + self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.executorClass = NSClassFromString(_settings[@"executorClass"]); + }); } - (void)jsLoaded @@ -147,10 +149,12 @@ RCT_EXPORT_MODULE() _liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL]; } - // Hit these setters again after bridge has finished loading - self.profilingEnabled = _profilingEnabled; - self.liveReloadEnabled = _liveReloadEnabled; - self.executorClass = _executorClass; + dispatch_async(dispatch_get_main_queue(), ^{ + // Hit these setters again after bridge has finished loading + self.profilingEnabled = _profilingEnabled; + self.liveReloadEnabled = _liveReloadEnabled; + self.executorClass = _executorClass; + }); } - (void)dealloc diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index 8d52529e6..01f51d7e9 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -22,6 +22,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; -- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(RCTJavaScriptCompleteBlock)onComplete; +- (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(void (^)(NSError *, NSString *))onComplete; @end diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 2e7d21b94..0210986dc 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -27,7 +27,7 @@ return self; } -- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete +- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *, NSString *))onComplete { // Sanitize the script URL scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; @@ -37,7 +37,7 @@ NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; - onComplete(error); + onComplete(error, nil); return; } @@ -57,7 +57,7 @@ code:error.code userInfo:userInfo]; } - onComplete(error); + onComplete(error, nil); return; } @@ -96,18 +96,10 @@ code:[(NSHTTPURLResponse *)response statusCode] userInfo:userInfo]; - onComplete(error); + onComplete(error, nil); return; } - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = scriptURL; - sourceCodeModule.scriptText = rawText; - - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(scriptError); - }); - }]; + onComplete(nil, rawText); }]; [task resume]; diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index ee5a35d7f..d55094c37 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,7 +11,7 @@ #import "RCTBridge.h" -@interface RCTRootView : UIView +@interface RCTRootView : UIView /** * - Designated initializer - diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 54556d418..9ee09d495 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -20,25 +20,37 @@ #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" +#import "RCTView.h" #import "RCTWebViewExecutor.h" #import "UIView+React.h" +@interface RCTBridge (RCTRootView) + +@property (nonatomic, weak, readonly) RCTBridge *batchedBridge; + +@end + @interface RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag; @end +@interface RCTRootContentView : RCTView + +- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; + +@end + @implementation RCTRootView { RCTBridge *_bridge; - RCTTouchHandler *_touchHandler; NSString *_moduleName; NSDictionary *_launchOptions; - UIView *_contentView; + RCTRootContentView *_contentView; } -- (instancetype)initWithBridge:(RCTBridge *)bridge + - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName { RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); @@ -52,11 +64,11 @@ _moduleName = moduleName; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(bundleFinishedLoading) + selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; - if (!_bridge.loading) { - [self bundleFinishedLoading]; + if (!_bridge.batchedBridge.isLoading) { + [self bundleFinishedLoading:_bridge.batchedBridge]; } } return self; @@ -73,25 +85,6 @@ return [self initWithBridge:bridge moduleName:moduleName]; } -- (BOOL)isValid -{ - return _contentView.userInteractionEnabled; -} - -- (void)invalidate -{ - _contentView.userInteractionEnabled = NO; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - if (_contentView) { - [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" - args:@[_contentView.reactTag]]; - } -} - - (UIViewController *)backingViewController { return _backingViewController ?: [super backingViewController]; @@ -105,9 +98,19 @@ RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) -- (void)bundleFinishedLoading + +- (void)javaScriptDidLoad:(NSNotification *)notification +{ + RCTBridge *bridge = notification.userInfo[@"bridge"]; + [self bundleFinishedLoading:bridge]; +} + +- (void)bundleFinishedLoading:(RCTBridge *)bridge { dispatch_async(dispatch_get_main_queue(), ^{ + if (!bridge.isValid) { + return; + } /** * Every root view that is created must have a unique React tag. @@ -117,19 +120,16 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) * the React tag is assigned every time we load new content. */ [_contentView removeFromSuperview]; - _contentView = [[UIView alloc] initWithFrame:self.bounds]; - _contentView.reactTag = [_bridge.uiManager allocateRootTag]; - _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; - [_contentView addGestureRecognizer:_touchHandler]; + _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds + bridge:bridge]; [self addSubview:_contentView]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, - @"initialProps": self.initialProperties ?: @{}, + @"initialProps": _initialProperties ?: @{}, }; - [_bridge.uiManager registerRootView:_contentView]; - [_bridge enqueueJSCall:@"AppRegistry.runApplication" + [bridge enqueueJSCall:@"AppRegistry.runApplication" args:@[moduleName, appParameters]]; }); } @@ -139,7 +139,6 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) [super layoutSubviews]; if (_contentView) { _contentView.frame = self.bounds; - [_bridge.uiManager setFrame:self.bounds forRootView:_contentView]; } } @@ -148,6 +147,12 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) return _contentView.reactTag; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_contentView removeFromSuperview]; +} + @end @implementation RCTUIManager (RCTRootView) @@ -160,3 +165,60 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) } @end + +@implementation RCTRootContentView +{ + __weak RCTBridge *_bridge; + RCTTouchHandler *_touchHandler; +} + +- (instancetype)initWithFrame:(CGRect)frame + bridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + [self setUp]; + self.frame = frame; + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + if (self.reactTag && _bridge.isValid) { + [_bridge.uiManager setFrame:self.bounds forRootView:self]; + } +} + +- (void)setUp +{ + /** + * Every root view that is created must have a unique react tag. + * Numbering of these tags goes from 1, 11, 21, 31, etc + * + * NOTE: Since the bridge persists, the RootViews might be reused, so now + * the react tag is assigned every time we load new content. + */ + self.reactTag = [_bridge.uiManager allocateRootTag]; + [self addGestureRecognizer:[[RCTTouchHandler alloc] initWithBridge:_bridge]]; + [_bridge.uiManager registerRootView:self]; +} + +- (BOOL)isValid +{ + return self.userInteractionEnabled; +} + +- (void)invalidate +{ + self.userInteractionEnabled = NO; +} + +- (void)dealloc +{ + [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" + args:@[self.reactTag]]; +} + +@end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index e2df5befc..e21c9d16f 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -70,6 +70,7 @@ } @synthesize bridge = _bridge; +@synthesize paused = _paused; RCT_EXPORT_MODULE() @@ -78,7 +79,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (instancetype)init { if ((self = [super init])) { - + _paused = YES; _timers = [[RCTSparseArray alloc] init]; for (NSString *name in @[UIApplicationWillResignActiveNotification, @@ -126,7 +127,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (void)stopTimers { - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; } - (void)startTimers @@ -135,7 +136,7 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) return; } - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } - (void)didUpdateFrame:(RCTFrameUpdate *)update diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 496b9c351..d35cae03a 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -267,11 +267,6 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa return self; } -- (void)dealloc -{ - RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); -} - - (BOOL)isValid { return _viewRegistry != nil; @@ -279,20 +274,24 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa - (void)invalidate { - RCTAssertMainThread(); + /** + * Called on the JS Thread since all modules are invalidated on the JS thread + */ - for (NSNumber *rootViewTag in _rootViewTags) { - ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; - } + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSNumber *rootViewTag in _rootViewTags) { + ((UIView *)_viewRegistry[rootViewTag]).userInteractionEnabled = NO; + } - _rootViewTags = nil; - _shadowViewRegistry = nil; - _viewRegistry = nil; - _bridge = nil; + _rootViewTags = nil; + _shadowViewRegistry = nil; + _viewRegistry = nil; + _bridge = nil; - [_pendingUIBlocksLock lock]; - _pendingUIBlocks = nil; - [_pendingUIBlocksLock unlock]; + [_pendingUIBlocksLock lock]; + _pendingUIBlocks = nil; + [_pendingUIBlocksLock unlock]; + }); } - (void)setBridge:(RCTBridge *)bridge diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index a4cb338fb..f947a6128 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -264,9 +264,13 @@ NSInteger kNeverProgressed = -10000; NSInteger _numberOfViewControllerMovesToIgnore; } +@synthesize paused = _paused; + - (id)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { + _paused = YES; + _bridge = bridge; _mostRecentProgress = kNeverProgressed; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; @@ -341,14 +345,14 @@ NSInteger kNeverProgressed = -10000; _dummyView.frame = (CGRect){{destination}}; _currentlyTransitioningFrom = indexOfFrom; _currentlyTransitioningTo = indexOfTo; - [_bridge addFrameUpdateObserver:self]; + _paused = NO; } completion:^(id context) { [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; _dummyView.frame = CGRectZero; - [_bridge removeFrameUpdateObserver:self]; + _paused = YES; // Reset the parallel position tracker }]; }