From cf38b083dda85d287a84c8ed0895a8b725eaad22 Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Fri, 11 Aug 2017 06:16:55 -0700 Subject: [PATCH] Implement lazy discovery scaffolding for loading NativeModules on demand. Reviewed By: javache Differential Revision: D5364734 fbshipit-source-id: 5162f7d41434a3ba38c82fa610e84f865bfacf50 --- React/Base/RCTBridge+Private.h | 5 + React/Base/RCTBridge.m | 5 + React/Base/RCTBridgeDelegate.h | 9 + React/CxxBridge/RCTCxxBridge.mm | 217 ++++++++++++++---------- ReactCommon/cxxreact/ModuleRegistry.cpp | 27 ++- ReactCommon/cxxreact/ModuleRegistry.h | 9 +- 6 files changed, 174 insertions(+), 98 deletions(-) diff --git a/React/Base/RCTBridge+Private.h b/React/Base/RCTBridge+Private.h index 9aeca9349..a917ef124 100644 --- a/React/Base/RCTBridge+Private.h +++ b/React/Base/RCTBridge+Private.h @@ -114,6 +114,11 @@ RCT_EXTERN void RCTVerifyAllModulesExported(NSArray *extraModules); */ - (RCTModuleData *)moduleDataForName:(NSString *)moduleName; +/** +* Registers additional classes with the ModuleRegistry. +*/ +- (void)registerAdditionalModuleClasses:(NSArray *)newModules; + /** * Systrace profiler toggling methods exposed for the RCTDevMenu */ diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 93d41779d..6a980d970 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -353,6 +353,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } +- (void)registerAdditionalModuleClasses:(NSArray *)modules +{ + [self.batchedBridge registerAdditionalModuleClasses:modules]; +} + - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; diff --git a/React/Base/RCTBridgeDelegate.h b/React/Base/RCTBridgeDelegate.h index 763933985..ab8c2e866 100644 --- a/React/Base/RCTBridgeDelegate.h +++ b/React/Base/RCTBridgeDelegate.h @@ -105,6 +105,15 @@ */ - (BOOL)shouldBridgeUseCxxBridge:(RCTBridge *)bridge; +/** +* The bridge will call this method when a module been called from JS +* cannot be found among registered modules. +* It should return YES if the module with name 'moduleName' was registered +* in the implementation, and the system must attempt to look for it again among registered. +* If the module was not registered, return NO to prevent further searches. +*/ +- (BOOL)bridge:(RCTBridge *)bridge didNotFindModule:(NSString *)moduleName; + /** * The bridge will automatically attempt to load the JS source code from the * location specified by the `sourceURLForBridge:` method, however, if you want diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 1729c603b..5ec148367 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -138,8 +138,8 @@ struct RCTInstanceCallback : public InstanceCallback { // Native modules NSMutableDictionary *_moduleDataByName; - NSArray *_moduleDataByID; - NSArray *_moduleClassesByID; + NSMutableArray *_moduleDataByID; + NSMutableArray *_moduleClassesByID; NSUInteger _modulesInitializedOnMainQueue; RCTDisplayLink *_displayLink; @@ -195,6 +195,10 @@ struct RCTInstanceCallback : public InstanceCallback { _pendingCalls = [NSMutableArray new]; _displayLink = [RCTDisplayLink new]; + _moduleDataByName = [NSMutableDictionary new]; + _moduleClassesByID = [NSMutableArray new]; + _moduleDataByID = [NSMutableArray new]; + [RCTBridge setCurrentBridge:self]; } return self; @@ -275,8 +279,13 @@ struct RCTInstanceCallback : public InstanceCallback { dispatch_group_t prepareBridge = dispatch_group_create(); + [_performanceLogger markStartForTag:RCTPLNativeModuleInit]; + + [self registerExtraModules]; // Initialize all native modules that cannot be loaded lazily - [self _initModulesWithDispatchGroup:prepareBridge]; + [self _initModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO]; + + [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; // This doesn't really do anything. The real work happens in initializeBridge. _reactInstance.reset(new Instance); @@ -442,7 +451,16 @@ struct RCTInstanceCallback : public InstanceCallback { [_performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig]; RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge buildModuleRegistry]", nil); - auto registry = std::make_shared(createNativeModules(_moduleDataByID, self, _reactInstance)); + __weak __typeof(self) weakSelf = self; + ModuleRegistry::ModuleNotFoundCallback moduleNotFoundCallback = ^bool(const std::string &name) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + return [strongSelf.delegate respondsToSelector:@selector(bridge:didNotFindModule:)] && + [strongSelf.delegate bridge:strongSelf didNotFindModule:@(name.c_str())]; + }; + + auto registry = std::make_shared( + createNativeModules(_moduleDataByID, self, _reactInstance), + moduleNotFoundCallback); [_performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); @@ -501,75 +519,24 @@ struct RCTInstanceCallback : public InstanceCallback { return moduleData.config; } -- (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup +- (NSArray *)registerModulesForClasses:(NSArray *)moduleClasses { - [_performanceLogger markStartForTag:RCTPLNativeModuleInit]; - - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, - @"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil); - NSArray> *extraModules = nil; - if (self.delegate) { - if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) { - extraModules = [self.delegate extraModulesForBridge:_parentBridge]; - } - } else if (self.moduleProvider) { - extraModules = self.moduleProvider(); - } - RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); - -#if RCT_DEBUG - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - RCTVerifyAllModulesExported(extraModules); - }); -#endif - - NSMutableArray *moduleClassesByID = [NSMutableArray new]; - NSMutableArray *moduleDataByID = [NSMutableArray new]; - NSMutableDictionary *moduleDataByName = [NSMutableDictionary new]; - - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, - @"-[RCTCxxBridge initModulesWithDispatchGroup:] preinitialized moduleData", nil); - // Set up moduleData for pre-initialized module instances - for (id module in extraModules) { - Class moduleClass = [module class]; - NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); - - if (RCT_DEBUG) { - // Check for name collisions between preregistered modules - RCTModuleData *moduleData = moduleDataByName[moduleName]; - if (moduleData) { - RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the " - "name '%@', but name was already registered by class %@", - moduleClass, moduleName, moduleData.moduleClass); - continue; - } - } - - // Instantiate moduleData container - RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module - bridge:self]; - moduleDataByName[moduleName] = moduleData; - [moduleClassesByID addObject:moduleClass]; - [moduleDataByID addObject:moduleData]; - } - RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); - RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil); - // Set up moduleData for automatically-exported modules - for (Class moduleClass in RCTGetModuleClasses()) { + + NSMutableArray *moduleDataByID = [NSMutableArray arrayWithCapacity:moduleClasses.count]; + for (Class moduleClass in moduleClasses) { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); // Don't initialize the old executor in the new bridge. // TODO mhorowitz #10487027: after D3175632 lands, we won't need // this, because it won't be eagerly initialized. - if ([moduleName isEqual:@"RCTJSCExecutor"]) { + if ([moduleName isEqualToString:@"RCTJSCExecutor"]) { continue; } // Check for module name collisions - RCTModuleData *moduleData = moduleDataByName[moduleName]; + RCTModuleData *moduleData = _moduleDataByName[moduleName]; if (moduleData) { if (moduleData.hasInstance) { // Existing module was preregistered, so it takes precedence @@ -587,44 +554,113 @@ struct RCTInstanceCallback : public InstanceCallback { // Instantiate moduleData // TODO #13258411: can we defer this until config generation? - moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass - bridge:self]; - moduleDataByName[moduleName] = moduleData; - [moduleClassesByID addObject:moduleClass]; + moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self]; + + _moduleDataByName[moduleName] = moduleData; + [_moduleClassesByID addObject:moduleClass]; [moduleDataByID addObject:moduleData]; } + [_moduleDataByID addObjectsFromArray:moduleDataByID]; + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); - // Store modules - _moduleDataByID = [moduleDataByID copy]; - _moduleDataByName = [moduleDataByName copy]; - _moduleClassesByID = [moduleClassesByID copy]; + return moduleDataByID; +} + +- (void)registerExtraModules +{ + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil); + + NSArray> *extraModules = nil; + if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) { + extraModules = [self.delegate extraModulesForBridge:_parentBridge]; + } else if (self.moduleProvider) { + extraModules = self.moduleProvider(); + } + + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + +#if RCT_DEBUG + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + RCTVerifyAllModulesExported(extraModules); + }); +#endif RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, - @"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil); - // Dispatch module init onto main thead for those modules that require it - for (RCTModuleData *moduleData in _moduleDataByID) { - if (moduleData.hasInstance && - (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) { - // Modules that were pre-initialized should ideally be set up before - // bridge init has finished, otherwise the caller may try to access the - // module directly rather than via `[bridge moduleForClass:]`, which won't - // trigger the lazy initialization process. If the module cannot safely be - // set up on the current thread, it will instead be async dispatched - // to the main thread to be set up in the loop below. - (void)[moduleData instance]; + @"-[RCTCxxBridge initModulesWithDispatchGroup:] preinitialized moduleData", nil); + // Set up moduleData for pre-initialized module instances + for (id module in extraModules) { + Class moduleClass = [module class]; + NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); + + if (RCT_DEBUG) { + // Check for name collisions between preregistered modules + RCTModuleData *moduleData = _moduleDataByName[moduleName]; + if (moduleData) { + RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the " + "name '%@', but name was already registered by class %@", + moduleClass, moduleName, moduleData.moduleClass); + continue; + } } + + // Instantiate moduleData container + RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module bridge:self]; + _moduleDataByName[moduleName] = moduleData; + [_moduleClassesByID addObject:moduleClass]; + [_moduleDataByID addObject:moduleData]; } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); +} - // From this point on, RCTDidInitializeModuleNotification notifications will - // be sent the first time a module is accessed. - _moduleSetupComplete = YES; +- (void)_initModules:(NSArray> *)modules + withDispatchGroup:(dispatch_group_t)dispatchGroup + lazilyDiscovered:(BOOL)lazilyDiscovered +{ + RCTAssert(!(RCTIsMainQueue() && lazilyDiscovered), @"Lazy discovery can only happen off the Main Queue"); - [self _prepareModulesWithDispatchGroup:dispatchGroup]; + // Set up moduleData for automatically-exported modules + NSArray *moduleDataById = [self registerModulesForClasses:modules]; - [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; - RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); +#ifdef RCT_DEBUG + if (lazilyDiscovered) { + // Lazily discovered modules do not require instantiation here, + // as they are not allowed to have pre-instantiated instance + // and must not require the main queue. + for (RCTModuleData *moduleData in moduleDataById) { + RCTAssert(!(moduleData.requiresMainQueueSetup || moduleData.hasInstance), + @"Module \'%@\' requires initialization on the Main Queue or has pre-instantiated, which is not supported for the lazily discovered modules.", moduleData.name); + } + } + else +#endif + { + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil); + // Dispatch module init onto main thread for those modules that require it + // For non-lazily discovered modules we run through the entire set of modules + // that we have, otherwise some modules coming from the delegate + // or module provider block, will not be properly instantiated. + for (RCTModuleData *moduleData in _moduleDataByID) { + if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) { + // Modules that were pre-initialized should ideally be set up before + // bridge init has finished, otherwise the caller may try to access the + // module directly rather than via `[bridge moduleForClass:]`, which won't + // trigger the lazy initialization process. If the module cannot safely be + // set up on the current thread, it will instead be async dispatched + // to the main thread to be set up in _prepareModulesWithDispatchGroup:. + (void)[moduleData instance]; + } + } + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + + // From this point on, RCTDidInitializeModuleNotification notifications will + // be sent the first time a module is accessed. + _moduleSetupComplete = YES; + [self _prepareModulesWithDispatchGroup:dispatchGroup]; + } #if RCT_PROFILE if (RCTProfileIsProfiling()) { @@ -634,6 +670,11 @@ struct RCTInstanceCallback : public InstanceCallback { #endif } +- (void)registerAdditionalModuleClasses:(NSArray *)modules +{ + [self _initModules:modules withDispatchGroup:NULL lazilyDiscovered:YES]; +} + - (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup { RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge prepareModulesWithDispatch]", nil); @@ -655,6 +696,7 @@ struct RCTInstanceCallback : public InstanceCallback { // Set up modules that require main thread init or constants export [_performanceLogger setValue:0 forTag:RCTPLNativeModuleMainThread]; + for (RCTModuleData *moduleData in _moduleDataByID) { if (whitelistedModules && ![whitelistedModules containsObject:[moduleData moduleClass]]) { continue; @@ -687,7 +729,6 @@ struct RCTInstanceCallback : public InstanceCallback { _modulesInitializedOnMainQueue++; } } - [_performanceLogger setValue:_modulesInitializedOnMainQueue forTag:RCTPLNativeModuleMainThreadUsesCount]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } diff --git a/ReactCommon/cxxreact/ModuleRegistry.cpp b/ReactCommon/cxxreact/ModuleRegistry.cpp index 907fce929..c91b01afc 100644 --- a/ReactCommon/cxxreact/ModuleRegistry.cpp +++ b/ReactCommon/cxxreact/ModuleRegistry.cpp @@ -27,8 +27,8 @@ std::string normalizeName(std::string name) { } -ModuleRegistry::ModuleRegistry(std::vector> modules) - : modules_(std::move(modules)) {} +ModuleRegistry::ModuleRegistry(std::vector> modules, ModuleNotFoundCallback callback) + : modules_{std::move(modules)}, moduleNotFoundCallback_{callback} {} void ModuleRegistry::updateModuleNamesFromIndex(size_t index) { for (; index < modules_.size(); index++ ) { @@ -82,13 +82,22 @@ folly::Optional ModuleRegistry::getConfig(const std::string& name) } auto it = modulesByName_.find(name); - if (it == modulesByName_.end()) { - unknownModules_.insert(name); - return nullptr; - } - CHECK(it->second < modules_.size()); - NativeModule* module = modules_[it->second].get(); + if (it == modulesByName_.end()) { + if (unknownModules_.find(name) != unknownModules_.end()) { + return nullptr; + } + if (!moduleNotFoundCallback_ || + !moduleNotFoundCallback_(name) || + (it = modulesByName_.find(name)) == modulesByName_.end()) { + unknownModules_.insert(name); + return nullptr; + } + } + size_t index = it->second; + + CHECK(index < modules_.size()); + NativeModule *module = modules_[index].get(); // string name, object constants, array methodNames (methodId is index), [array promiseMethodIds], [array syncMethodIds] folly::dynamic config = folly::dynamic::array(name); @@ -131,7 +140,7 @@ folly::Optional ModuleRegistry::getConfig(const std::string& name) // no constants or methods return nullptr; } else { - return ModuleConfig({it->second, config}); + return ModuleConfig{index, config}; } } diff --git a/ReactCommon/cxxreact/ModuleRegistry.h b/ReactCommon/cxxreact/ModuleRegistry.h index 83fe70b01..0b55d9ce5 100644 --- a/ReactCommon/cxxreact/ModuleRegistry.h +++ b/ReactCommon/cxxreact/ModuleRegistry.h @@ -33,7 +33,9 @@ class RN_EXPORT ModuleRegistry { // notifyCatalystInstanceInitialized: this is really only used by view-related code // notifyCatalystInstanceDestroy: use RAII instead - ModuleRegistry(std::vector> modules); + using ModuleNotFoundCallback = std::function; + + ModuleRegistry(std::vector> modules, ModuleNotFoundCallback callback = nullptr); void registerModules(std::vector> modules); std::vector moduleNames(); @@ -56,6 +58,11 @@ class RN_EXPORT ModuleRegistry { // This is populated with modules that are requested via getConfig but are unknown. // An error will be thrown if they are subsquently added to the registry. std::unordered_set unknownModules_; + + // Function will be called if a module was requested but was not found. + // If the function returns true, ModuleRegistry will try to find the module again (assuming it's registered) + // If the functon returns false, ModuleRegistry will not try to find the module and return nullptr instead. + ModuleNotFoundCallback moduleNotFoundCallback_; }; }