diff --git a/Libraries/Utilities/UIManager.js b/Libraries/Utilities/UIManager.js index e3d4271d5..95c290c25 100644 --- a/Libraries/Utilities/UIManager.js +++ b/Libraries/Utilities/UIManager.js @@ -16,8 +16,9 @@ const NativeModules = require('NativeModules'); const { UIManager } = NativeModules; const findNodeHandle = require('react/lib/findNodeHandle'); +const invariant = require('fbjs/lib/invariant'); -const _takeSnapshot = UIManager.takeSnapshot; +invariant(UIManager, 'UIManager is undefined. The native module config is probably incorrect.'); /** * Capture an image of the screen, window or an individual view. The image @@ -39,12 +40,13 @@ const _takeSnapshot = UIManager.takeSnapshot; UIManager.takeSnapshot = async function( view ?: 'window' | ReactElement | number, options ?: { - width ?: number; - height ?: number; - format ?: 'png' | 'jpeg'; - quality ?: number; + width ?: number, + height ?: number, + format ?: 'png' | 'jpeg', + quality ?: number, }, ) { + const _takeSnapshot = UIManager.takeSnapshot; if (!_takeSnapshot) { console.warn('UIManager.takeSnapshot is not available on this platform'); return; diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index baa19eff2..66cad87b1 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -46,9 +46,10 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); { BOOL _wasBatchActive; NSMutableArray *_pendingCalls; - NSMutableDictionary *_moduleDataByName; + NSDictionary *_moduleDataByName; NSArray *_moduleDataByID; NSArray *_moduleClassesByID; + NSUInteger _modulesInitializedOnMainQueue; RCTDisplayLink *_displayLink; } @@ -99,6 +100,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele - (void)start { + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge setUp]", nil); + dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t initModulesAndLoadSource = dispatch_group_create(); @@ -137,9 +140,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele // Asynchronously gather the module config dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ if (weakSelf.valid) { + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge moduleConfig", nil); [performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig]; config = [weakSelf moduleConfig]; [performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig]; + RCT_PROFILE_END_EVENT(0, @"", nil); } }); @@ -166,6 +171,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele [strongSelf executeSourceCode:sourceCode]; } }); + + RCT_PROFILE_END_EVENT(0, @"", nil); } - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad @@ -233,6 +240,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele moduleData = _moduleDataByName[[@"RCT" stringByAppendingString:moduleName]]; } if (moduleData) { +#if RCT_DEV + if ([self.delegate respondsToSelector:@selector(whitelistedModulesForBridge:)]) { + NSArray *whitelisted = [self.delegate whitelistedModulesForBridge:self]; + RCTAssert(!whitelisted || [whitelisted containsObject:[moduleData moduleClass]], + @"Required config for %@, which was not whitelisted", moduleName); + } +#endif return moduleData.config; } return (id)kCFNull; @@ -240,6 +254,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele - (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup { + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge initModules]", nil); [_performanceLogger markStartForTag:RCTPLNativeModuleInit]; NSArray> *extraModules = nil; @@ -252,7 +267,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele } if (RCT_DEBUG && !RCTRunningInTestEnvironment()) { - // Check for unexported modules static Class *classes; static unsigned int classCount; @@ -290,6 +304,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele NSMutableDictionary *moduleDataByName = [NSMutableDictionary new]; // Set up moduleData for pre-initialized module instances + RCT_PROFILE_BEGIN_EVENT(0, @"extraModules", nil); for (id module in extraModules) { Class moduleClass = [module class]; NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); @@ -317,11 +332,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele _javaScriptExecutor = (id)module; } } + RCT_PROFILE_END_EVENT(0, @"", nil); // The executor is a bridge module, but we want it to be instantiated before // any other module has access to the bridge, in case they need the JS thread. // TODO: once we have more fine-grained control of init (t11106126) we can // probably just replace this with [self moduleForClass:self.executorClass] + RCT_PROFILE_BEGIN_EVENT(0, @"JavaScriptExecutor", nil); if (!_javaScriptExecutor) { id executorModule = [self.executorClass new]; RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:executorModule @@ -333,8 +350,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele // NOTE: _javaScriptExecutor is a weak reference _javaScriptExecutor = executorModule; } + RCT_PROFILE_END_EVENT(0, @"", nil); // Set up moduleData for automatically-exported modules + RCT_PROFILE_BEGIN_EVENT(0, @"ModuleData", nil); for (Class moduleClass in RCTGetModuleClasses()) { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); @@ -367,8 +386,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele _moduleDataByID = [moduleDataByID copy]; _moduleDataByName = [moduleDataByName copy]; _moduleClassesByID = [moduleClassesByID copy]; + RCT_PROFILE_END_EVENT(0, @"", nil); // Synchronously set up the pre-initialized modules + RCT_PROFILE_BEGIN_EVENT(0, @"extraModules", nil); for (RCTModuleData *moduleData in _moduleDataByID) { if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) { @@ -381,39 +402,80 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)dele (void)[moduleData instance]; } } + RCT_PROFILE_END_EVENT(0, @"", nil); // From this point on, RCTDidInitializeModuleNotification notifications will // be sent the first time a module is accessed. _moduleSetupComplete = YES; + [self prepareModulesWithDispatchGroup:dispatchGroup]; + + [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; + RCT_PROFILE_END_EVENT(0, @"", nil); +} + +- (void)prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup +{ + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge prepareModulesWithDispatch]", nil); + + NSArray *whitelistedModules = nil; + if ([self.delegate respondsToSelector:@selector(whitelistedModulesForBridge:)]) { + whitelistedModules = [self.delegate whitelistedModulesForBridge:self]; + } + + BOOL initializeImmediately = NO; + if (dispatchGroup == NULL) { + // If no dispatchGroup is passed in, we must prepare everything immediately. + // We better be on the right thread too. + RCTAssertMainQueue(); + initializeImmediately = YES; + } else if ([self.delegate respondsToSelector:@selector(shouldBridgeInitializeNativeModulesSynchronously:)]) { + initializeImmediately = [self.delegate shouldBridgeInitializeNativeModulesSynchronously:self]; + } + // Set up modules that require main thread init or constants export [_performanceLogger setValue:0 forTag:RCTPLNativeModuleMainThread]; - NSUInteger modulesOnMainQueueCount = 0; for (RCTModuleData *moduleData in _moduleDataByID) { - __weak RCTBatchedBridge *weakSelf = self; + if (whitelistedModules && ![whitelistedModules containsObject:[moduleData moduleClass]]) { + continue; + } + if (moduleData.requiresMainQueueSetup || moduleData.hasConstantsToExport) { // Modules that need to be set up on the main thread cannot be initialized // lazily when required without doing a dispatch_sync to the main thread, // which can result in deadlock. To avoid this, we initialize all of these // modules on the main thread in parallel with loading the JS code, so // they will already be available before they are ever required. - dispatch_group_async(dispatchGroup, dispatch_get_main_queue(), ^{ - RCTBatchedBridge *strongSelf = weakSelf; - if (!strongSelf.valid) { - return; + dispatch_block_t block = ^{ + if (self.valid) { + [self->_performanceLogger appendStartForTag:RCTPLNativeModuleMainThread]; + (void)[moduleData instance]; + [moduleData gatherConstants]; + [self->_performanceLogger appendStopForTag:RCTPLNativeModuleMainThread]; } + }; - [strongSelf->_performanceLogger appendStartForTag:RCTPLNativeModuleMainThread]; - (void)[moduleData instance]; - [moduleData gatherConstants]; - [strongSelf->_performanceLogger appendStopForTag:RCTPLNativeModuleMainThread]; - }); - modulesOnMainQueueCount++; + if (initializeImmediately && RCTIsMainQueue()) { + block(); + } else { + // We've already checked that dispatchGroup is non-null, but this satisifies the + // Xcode analyzer + if (dispatchGroup) { + dispatch_group_async(dispatchGroup, dispatch_get_main_queue(), block); + } + } + _modulesInitializedOnMainQueue++; } } - [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; - [_performanceLogger setValue:modulesOnMainQueueCount forTag:RCTPLNativeModuleMainThreadUsesCount]; + [_performanceLogger setValue:_modulesInitializedOnMainQueue forTag:RCTPLNativeModuleMainThreadUsesCount]; + RCT_PROFILE_END_EVENT(0, @"", nil); +} + +- (void)whitelistedModulesDidChange +{ + RCTAssertMainQueue(); + [self prepareModulesWithDispatchGroup:NULL]; } - (void)setUpExecutor @@ -597,23 +659,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR queue:(dispatch_queue_t)queue { if (queue == RCTJSThread) { - __weak __typeof(self) weakSelf = self; RCTProfileBeginFlowEvent(); RCTAssert(_javaScriptExecutor != nil, @"Need JS executor to schedule JS work"); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEndFlowEvent(); - RCTBatchedBridge *strongSelf = weakSelf; - if (!strongSelf) { - return; - } + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge dispatchBlock", @{ @"loading": @(self.loading) }); - if (strongSelf.loading) { - [strongSelf->_pendingCalls addObject:block]; + if (self.loading) { + RCTAssert(self->_pendingCalls != nil, @"Can't add pending call, bridge is no longer loading"); + [self->_pendingCalls addObject:block]; } else { block(); } + + RCT_PROFILE_END_EVENT(0, @"", nil); }]; } else if (queue) { dispatch_async(queue, block); @@ -705,11 +766,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR /** * AnyThread */ - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTBatchedBridge enqueueJSCall:]", nil); if (!_valid) { return; } + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTBatchedBridge enqueueJSCall:]", nil); __weak __typeof(self) weakSelf = self; [self dispatchBlock:^{ [weakSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]]; @@ -717,7 +778,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR completion(); } } queue:RCTJSThread]; - RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"", nil); } @@ -791,7 +851,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR { RCTAssertJSThread(); - __weak typeof(self) weakSelf = self; + __weak __typeof(self) weakSelf = self; [_javaScriptExecutor callFunctionOnModule:module method:method arguments:args @@ -805,7 +865,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR { RCTAssertJSThread(); - __weak typeof(self) weakSelf = self; + __weak __typeof(self) weakSelf = self; [_javaScriptExecutor invokeCallbackID:cbID arguments:args callback:^(id json, NSError *error) { @@ -931,7 +991,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR - (void)partialBatchDidFlush { for (RCTModuleData *moduleData in _moduleDataByID) { - if (moduleData.implementsPartialBatchDidFlush) { + if (moduleData.hasInstance && moduleData.implementsPartialBatchDidFlush) { [self dispatchBlock:^{ [moduleData.instance partialBatchDidFlush]; } queue:moduleData.methodQueue]; @@ -943,7 +1003,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR { // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? for (RCTModuleData *moduleData in _moduleDataByID) { - if (moduleData.implementsBatchDidComplete) { + if (moduleData.hasInstance && moduleData.implementsBatchDidComplete) { [self dispatchBlock:^{ [moduleData.instance batchDidComplete]; } queue:moduleData.methodQueue]; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index a3e2b500d..944997666 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -126,6 +126,17 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); */ - (BOOL)moduleIsInitialized:(Class)moduleClass; +/** + * Call when your delegate's `whitelistedModulesForBridge:` value has changed. + * In response to this, the bridge will immediately instantiate any (whitelisted) + * native modules that require main thread initialization. Modules that do not require + * main thread initialization will still be created lazily. + * + * This method must be called on the main thread, as any pending native modules + * will be initialized immediately. + */ +- (void)whitelistedModulesDidChange; + /** * All registered bridge module classes. */ diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index f515a8731..c4ccf4e66 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -17,6 +17,7 @@ #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTPerformanceLogger.h" +#import "RCTProfile.h" #import "RCTUtils.h" NSString *const RCTReloadNotification = @"RCTReloadNotification"; @@ -176,7 +177,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) object:nil userInfo:nil]; }]; - #endif } @@ -219,6 +219,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return [self.batchedBridge moduleIsInitialized:moduleClass]; } +- (void)whitelistedModulesDidChange +{ + [self.batchedBridge whitelistedModulesDidChange]; +} + - (void)reload { /** @@ -232,6 +237,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)setUp { + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBridge setUp]", nil); + // Only update bundleURL from delegate if delegate bundleURL has changed NSURL *previousDelegateURL = _delegateBundleURL; _delegateBundleURL = [self.delegate sourceURLForBridge:self]; @@ -243,6 +250,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) _bundleURL = [RCTConvert NSURL:_bundleURL.absoluteString]; [self createBatchedBridge]; + + RCT_PROFILE_END_EVENT(0, @"", nil); } - (void)createBatchedBridge diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index 02da39bda..f29c7aae4 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -7,10 +7,11 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -@class RCTBridge; - #import "RCTJavaScriptLoader.h" +@class RCTBridge; +@protocol RCTBridgeModule; + @protocol RCTBridgeDelegate /** @@ -46,7 +47,31 @@ * not recommended in most cases - if the module methods and behavior do not * match exactly, it may lead to bugs or crashes. */ -- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge; +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge; + +/** + * Customize how bridge native modules are initialized. + * + * By default all modules are created lazily except those that have constants to export + * or require main thread initialization. If you want to limit the set of native + * modules that this should be considered for, implement this method. + * + * Return nil to whitelist all modules found. Modules passed in extraModulesForBridge: + * are automatically whitelisted. + * + * @experimental + */ +- (NSArray *)whitelistedModulesForBridge:(RCTBridge *)bridge; + +/** + * When initializing native modules that require main thread initialization, the bridge + * will default to dispatch module creation blocks asynchrously. If we're blockingly + * waiting on the main thread to finish bridge creation on the main thread, this will + * deadlock. Override this method to initialize modules synchronously instead. + * + * @experimental + */ +- (BOOL)shouldBridgeInitializeNativeModulesSynchronously:(RCTBridge *)bridge; /** * The bridge will automatically attempt to load the JS source code from the diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index 30e5141a1..e1a113212 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -83,14 +83,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); - (void)setUpInstanceAndBridge { - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpInstanceAndBridge] [_instanceLock lock]", nil); + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpInstanceAndBridge] [_instanceLock lock]", @{ @"moduleClass": _moduleClass }); [_instanceLock lock]; if (!_setupComplete && _bridge.valid) { if (!_instance) { if (RCT_DEBUG && _requiresMainQueueSetup) { RCTAssertMainQueue(); } - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpInstanceAndBridge] [_moduleClass new]", nil); + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData setUpInstanceAndBridge] [_moduleClass new]", @{ @"moduleClass": _moduleClass }); _instance = [_moduleClass new]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"", nil); if (!_instance) { @@ -216,6 +216,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); // get accessed by client code during bridge setup, and a very low risk of // deadlock is better than a fairly high risk of an assertion being thrown. RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"[RCTModuleData instance] main thread setup", nil); + + if (!RCTIsMainQueue()) { + RCTLogWarn(@"RCTBridge required dispatch_sync to load %@. This may lead to deadlocks", _moduleClass); + } RCTExecuteOnMainThread(^{ [self setUpInstanceAndBridge]; }, YES); @@ -278,6 +282,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init); if (_hasConstantsToExport && !_constantsToExport) { RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, [NSString stringWithFormat:@"[RCTModuleData gatherConstants] %@", _moduleClass], nil); (void)[self instance]; + if (!RCTIsMainQueue()) { + RCTLogWarn(@"Required dispatch_sync to load constants for %@. This may lead to deadlocks", _moduleClass); + } RCTExecuteOnMainThread(^{ self->_constantsToExport = [self->_instance constantsToExport] ?: @{}; }, YES);