/** * 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. */ #import #import "RCTAssert.h" #import "RCTBridge+Private.h" #import "RCTBridge.h" #import "RCTBridgeMethod.h" #import "RCTConvert.h" #import "RCTDevLoadingView.h" #import "RCTDisplayLink.h" #import "RCTJSCExecutor.h" #import "RCTJavaScriptLoader.h" #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTPerformanceLogger.h" #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTUtils.h" #define RCTAssertJSThread() \ RCTAssert(![NSStringFromClass([self->_javaScriptExecutor class]) isEqualToString:@"RCTJSCExecutor"] || \ [[[NSThread currentThread] name] isEqualToString:RCTJSCThreadName], \ @"This method must be called on JS thread") /** * Must be kept in sync with `MessageQueue.js`. */ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldRequestModuleIDs = 0, RCTBridgeFieldMethodIDs, RCTBridgeFieldParams, RCTBridgeFieldCallID, }; @implementation RCTBatchedBridge { BOOL _wasBatchActive; NSMutableArray *_pendingCalls; NSDictionary *_moduleDataByName; NSArray *_moduleDataByID; NSArray *_moduleClassesByID; NSUInteger _modulesInitializedOnMainQueue; RCTDisplayLink *_displayLink; } @synthesize flowID = _flowID; @synthesize flowIDMap = _flowIDMap; @synthesize flowIDMapLock = _flowIDMapLock; @synthesize loading = _loading; @synthesize valid = _valid; @synthesize performanceLogger = _performanceLogger; - (instancetype)initWithParentBridge:(RCTBridge *)bridge { RCTAssertParam(bridge); if (self = [super initWithDelegate:bridge.delegate bundleURL:bridge.bundleURL moduleProvider:bridge.moduleProvider launchOptions:bridge.launchOptions]) { _parentBridge = bridge; _performanceLogger = [bridge performanceLogger]; RCTLogInfo(@"Initializing %@ (parent: %@, executor: %@)", self, bridge, [self executorClass]); /** * Set Initial State */ _valid = YES; _loading = YES; _pendingCalls = [NSMutableArray new]; _displayLink = [RCTDisplayLink new]; [RCTBridge setCurrentBridge:self]; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id)delegate bundleURL:(NSURL *)bundleURL moduleProvider:(RCTBridgeModuleProviderBlock)block launchOptions:(NSDictionary *)launchOptions) - (void)start { [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptWillStartLoadingNotification object:_parentBridge userInfo:@{@"bridge": self}]; 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(); // Asynchronously load source code dispatch_group_enter(initModulesAndLoadSource); __weak RCTBatchedBridge *weakSelf = self; __block NSData *sourceCode; [self loadSource:^(NSError *error, NSData *source, __unused int64_t sourceLength) { if (error) { RCTLogWarn(@"Failed to load source: %@", error); dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf stopLoadingWithError:error]; }); } sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); } onProgress:^(RCTLoadingProgress *progressData) { #ifdef RCT_DEV RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]]; [loadingView updateProgress:progressData]; #endif }]; // Synchronously initialize all native modules that cannot be loaded lazily [self initModulesWithDispatchGroup:initModulesAndLoadSource]; RCTPerformanceLogger *performanceLogger = self->_performanceLogger; __block NSString *config; dispatch_group_enter(initModulesAndLoadSource); dispatch_async(bridgeQueue, ^{ dispatch_group_t setupJSExecutorAndModuleConfig = dispatch_group_create(); // Asynchronously initialize the JS executor dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ [performanceLogger markStartForTag:RCTPLJSCExecutorSetup]; [weakSelf setUpExecutor]; [performanceLogger markStopForTag:RCTPLJSCExecutorSetup]; }); // 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(RCTProfileTagAlways, @""); } }); dispatch_group_notify(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ // We're not waiting for this to complete to leave dispatch group, since // injectJSONConfiguration and executeSourceCode will schedule operations // on the same queue anyway. [performanceLogger markStartForTag:RCTPLNativeModuleInjectConfig]; [weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) { [performanceLogger markStopForTag:RCTPLNativeModuleInjectConfig]; if (error) { RCTLogWarn(@"Failed to inject config: %@", error); dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf stopLoadingWithError:error]; }); } }]; dispatch_group_leave(initModulesAndLoadSource); }); }); dispatch_group_notify(initModulesAndLoadSource, bridgeQueue, ^{ RCTBatchedBridge *strongSelf = weakSelf; if (sourceCode && strongSelf.loading) { [strongSelf executeSourceCode:sourceCode]; } }); RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress { [_performanceLogger markStartForTag:RCTPLScriptDownload]; RCTPerformanceLogger *performanceLogger = _performanceLogger; RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSData *source, int64_t sourceLength) { [performanceLogger markStopForTag:RCTPLScriptDownload]; [performanceLogger setValue:sourceLength forTag:RCTPLBundleSize]; _onSourceLoad(error, source, sourceLength); }; if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) { [self.delegate loadSourceForBridge:_parentBridge onProgress:onProgress onComplete:onSourceLoad]; } else if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad]; } else { RCTAssert(self.bundleURL, @"bundleURL must be non-nil when not implementing loadSourceForBridge"); [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, NSData *source, int64_t sourceLength) { if (error && [self.delegate respondsToSelector:@selector(fallbackSourceURLForBridge:)]) { NSURL *fallbackURL = [self.delegate fallbackSourceURLForBridge:self->_parentBridge]; if (fallbackURL && ![fallbackURL isEqual:self.bundleURL]) { RCTLogError(@"Failed to load bundle(%@) with error:(%@)", self.bundleURL, error.localizedDescription); self.bundleURL = fallbackURL; [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:onSourceLoad]; return; } } onSourceLoad(error, source, sourceLength); }]; } } - (NSArray *)moduleClasses { if (RCT_DEBUG && _valid && _moduleClassesByID == nil) { RCTLogError(@"Bridge modules have not yet been initialized. You may be " "trying to access a module too early in the startup procedure."); } return _moduleClassesByID; } /** * Used by RCTUIManager */ - (RCTModuleData *)moduleDataForName:(NSString *)moduleName { return _moduleDataByName[moduleName]; } - (id)moduleForName:(NSString *)moduleName { return _moduleDataByName[moduleName].instance; } - (BOOL)moduleIsInitialized:(Class)moduleClass { return _moduleDataByName[RCTBridgeModuleNameForClass(moduleClass)].hasInstance; } - (NSArray *)configForModuleName:(NSString *)moduleName { RCTModuleData *moduleData = _moduleDataByName[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; } - (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup { RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge initModules]", nil); [_performanceLogger markStartForTag:RCTPLNativeModuleInit]; NSArray> *extraModules = nil; if (self.delegate) { if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) { extraModules = [self.delegate extraModulesForBridge:_parentBridge]; } } else if (self.moduleProvider) { extraModules = self.moduleProvider(); } #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]; // 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); 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]; // Set executor instance if (moduleClass == self.executorClass) { _javaScriptExecutor = (id)module; } } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); // 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 bridge:self]; moduleDataByName[moduleData.name] = moduleData; [moduleClassesByID addObject:self.executorClass]; [moduleDataByID addObject:moduleData]; // NOTE: _javaScriptExecutor is a weak reference _javaScriptExecutor = executorModule; } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); // Set up moduleData for automatically-exported modules RCT_PROFILE_BEGIN_EVENT(0, @"ModuleData", nil); for (Class moduleClass in RCTGetModuleClasses()) { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); // Check for module name collisions RCTModuleData *moduleData = moduleDataByName[moduleName]; if (moduleData) { if (moduleData.hasInstance) { // Existing module was preregistered, so it takes precedence continue; } else if ([moduleClass new] == nil) { // The new module returned nil from init, so use the old module continue; } else if ([moduleData.moduleClass new] != nil) { // Both modules were non-nil, so it's unclear which should take precedence RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the " "name '%@', but name was already registered by class %@", moduleClass, moduleName, moduleData.moduleClass); } } // Instantiate moduleData (TODO: can we defer this until config generation?) moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self]; moduleDataByName[moduleName] = moduleData; [moduleClassesByID addObject:moduleClass]; [moduleDataByID addObject:moduleData]; } // Store modules _moduleDataByID = [moduleDataByID copy]; _moduleDataByName = [moduleDataByName copy]; _moduleClassesByID = [moduleClassesByID copy]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); // 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())) { // 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]; } } 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]; [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } - (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 for (RCTModuleData *moduleData in _moduleDataByID) { 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_block_t block = ^{ if (self.valid) { [self->_performanceLogger appendStartForTag:RCTPLNativeModuleMainThread]; (void)[moduleData instance]; [moduleData gatherConstants]; [self->_performanceLogger appendStopForTag:RCTPLNativeModuleMainThread]; } }; 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 setValue:_modulesInitializedOnMainQueue forTag:RCTPLNativeModuleMainThreadUsesCount]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } - (void)whitelistedModulesDidChange { RCTAssertMainQueue(); [self prepareModulesWithDispatchGroup:NULL]; } - (void)setUpExecutor { [_javaScriptExecutor setUp]; } - (void)registerModuleForFrameUpdates:(id)module withModuleData:(RCTModuleData *)moduleData { [_displayLink registerModuleForFrameUpdates:module withModuleData:moduleData]; } - (NSString *)moduleConfig { NSMutableArray *config = [NSMutableArray new]; for (RCTModuleData *moduleData in _moduleDataByID) { if (self.executorClass == [RCTJSCExecutor class]) { [config addObject:@[moduleData.name]]; } else { [config addObject:RCTNullIfNil(moduleData.config)]; } } return RCTJSONStringify(@{ @"remoteModuleConfig": config, }, NULL); } - (void)injectJSONConfiguration:(NSString *)configJSON onComplete:(void (^)(NSError *))onComplete { if (!_valid) { return; } [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:onComplete]; } - (void)executeSourceCode:(NSData *)sourceCode { if (!_valid || !_javaScriptExecutor) { return; } [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) { if (!self->_valid) { return; } if (loadError) { RCTLogWarn(@"Failed to execute source code: %@", [loadError localizedDescription]); dispatch_async(dispatch_get_main_queue(), ^{ [self stopLoadingWithError:loadError]; }); return; } // Register the display link to start sending js calls after everything is setup NSRunLoop *targetRunLoop = [self->_javaScriptExecutor isKindOfClass:[RCTJSCExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; [self->_displayLink addToRunLoop:targetRunLoop]; // Log metrics about native requires during the bridge startup. uint64_t nativeRequiresCount = [self->_performanceLogger valueForTag:RCTPLRAMNativeRequiresCount]; [self->_performanceLogger setValue:nativeRequiresCount forTag:RCTPLRAMStartupNativeRequiresCount]; uint64_t nativeRequires = [self->_performanceLogger valueForTag:RCTPLRAMNativeRequires]; [self->_performanceLogger setValue:nativeRequires forTag:RCTPLRAMStartupNativeRequires]; [self->_performanceLogger markStopForTag:RCTPLBridgeStartup]; // Perform the notification on the main thread, so we can't run into // timing issues with RCTRootView dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification object:self->_parentBridge userInfo:@{@"bridge": self}]; }); [self _flushPendingCalls]; }]; #if RCT_DEV if ([RCTGetURLQueryParam(self.bundleURL, @"hot") boolValue]) { NSString *path = [self.bundleURL.path substringFromIndex:1]; // strip initial slash NSString *host = self.bundleURL.host; NSNumber *port = self.bundleURL.port; [self enqueueJSCall:@"HMRClient" method:@"enable" args:@[@"ios", path, host, RCTNullIfNil(port)] completion:NULL]; } #endif } - (void)_flushPendingCalls { RCTAssertJSThread(); RCT_PROFILE_BEGIN_EVENT(0, @"Processing pendingCalls", @{ @"count": @(_pendingCalls.count) }); _loading = NO; NSArray *pendingCalls = _pendingCalls; _pendingCalls = nil; for (dispatch_block_t call in pendingCalls) { call(); } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } - (void)stopLoadingWithError:(NSError *)error { RCTAssertMainQueue(); if (!_valid || !_loading) { return; } _loading = NO; [_javaScriptExecutor invalidate]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification object:_parentBridge userInfo:@{@"bridge": self, @"error": error}]; if ([error userInfo][RCTJSStackTraceKey]) { [self.redBox showErrorMessage:[error localizedDescription] withStack:[error userInfo][RCTJSStackTraceKey]]; } RCTFatal(error); } RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL moduleProvider:(__unused RCTBridgeModuleProviderBlock)block launchOptions:(__unused NSDictionary *)launchOptions) /** * Prevent super from calling setUp (that'd create another batchedBridge) */ - (void)setUp {} - (void)bindKeys {} - (void)reload { [_parentBridge reload]; } - (void)requestReload { [_parentBridge requestReload]; } - (Class)executorClass { return _parentBridge.executorClass ?: [RCTJSCExecutor class]; } - (void)setExecutorClass:(Class)executorClass { RCTAssertMainQueue(); _parentBridge.executorClass = executorClass; } - (NSURL *)bundleURL { return _parentBridge.bundleURL; } - (void)setBundleURL:(NSURL *)bundleURL { _parentBridge.bundleURL = bundleURL; } - (id)delegate { return _parentBridge.delegate; } - (BOOL)isLoading { return _loading; } - (BOOL)isValid { return _valid; } - (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue { if (queue == RCTJSThread) { RCTProfileBeginFlowEvent(); RCTAssert(_javaScriptExecutor != nil, @"Need JS executor to schedule JS work"); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEndFlowEvent(); RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge dispatchBlock", @{ @"loading": @(self.loading) }); 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(RCTProfileTagAlways, @""); }]; } else if (queue) { dispatch_async(queue, block); } } #pragma mark - RCTInvalidating - (void)invalidate { if (!_valid) { return; } RCTAssertMainQueue(); RCTAssert(_javaScriptExecutor != nil, @"Can't complete invalidation without a JS executor"); _loading = NO; _valid = NO; if ([RCTBridge currentBridge] == self) { [RCTBridge setCurrentBridge:nil]; } // Invalidate modules dispatch_group_t group = dispatch_group_create(); for (RCTModuleData *moduleData in _moduleDataByID) { // Be careful when grabbing an instance here, we don't want to instantiate // any modules just to invalidate them. id instance = nil; if ([moduleData hasInstance]) { instance = moduleData.instance; } if (instance == _javaScriptExecutor) { continue; } if ([instance respondsToSelector:@selector(invalidate)]) { dispatch_group_enter(group); [self dispatchBlock:^{ [(id)instance invalidate]; dispatch_group_leave(group); } queue:moduleData.methodQueue]; } [moduleData invalidate]; } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ [self->_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ [self->_displayLink invalidate]; self->_displayLink = nil; [self->_javaScriptExecutor invalidate]; self->_javaScriptExecutor = nil; if (RCTProfileIsProfiling()) { RCTProfileUnhookModules(self); } self->_moduleDataByName = nil; self->_moduleDataByID = nil; self->_moduleClassesByID = nil; self->_pendingCalls = nil; if (self->_flowIDMap != NULL) { CFRelease(self->_flowIDMap); } }]; }); } - (void)logMessage:(NSString *)message level:(NSString *)level { if (RCT_DEBUG && [_javaScriptExecutor isValid]) { [self enqueueJSCall:@"RCTLog" method:@"logIfNoNativeHook" args:@[level, message] completion:NULL]; } } #pragma mark - RCTBridge methods /** * Public. Can be invoked from any thread. */ - (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion { /** * AnyThread */ 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 ?: @[]]; if (completion) { completion(); } } queue:RCTJSThread]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); } /** * Called by RCTModuleMethod from any thread. */ - (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args { /** * AnyThread */ if (!_valid) { return; } __weak __typeof(self) weakSelf = self; [self dispatchBlock:^{ [weakSelf _actuallyInvokeCallback:cbID arguments:args]; } queue:RCTJSThread]; } /** * JS thread only */ - (JSValue *)callFunctionOnModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)arguments error:(NSError **)error { RCTJSCExecutor *jsExecutor = (RCTJSCExecutor *)_javaScriptExecutor; if (![jsExecutor isKindOfClass:[RCTJSCExecutor class]]) { RCTLogWarn(@"FBReactBridgeJSExecutor is only supported when running in JSC"); return nil; } __block JSValue *jsResult = nil; RCTAssertJSThread(); RCT_PROFILE_BEGIN_EVENT(0, @"callFunctionOnModule", (@{ @"module": module, @"method": method })); [jsExecutor callFunctionOnModule:module method:method arguments:arguments ?: @[] jsValueCallback:^(JSValue *result, NSError *jsError) { if (error) { *error = jsError; } JSValue *length = result[@"length"]; RCTAssert([length isNumber] && [length toUInt32] == 2, @"Return value of a callFunction must be an array of size 2"); jsResult = [result valueAtIndex:0]; NSArray *nativeModuleCalls = [[result valueAtIndex:1] toArray]; [self handleBuffer:nativeModuleCalls batchEnded:YES]; }]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call"); return jsResult; } /** * Private hack to support `setTimeout(fn, 0)` */ - (void)_immediatelyCallTimer:(NSNumber *)timer { RCTAssertJSThread(); [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:^{ [self _actuallyInvokeAndProcessModule:@"JSTimersExecution" method:@"callTimers" arguments:@[@[timer]]]; }]; } - (void)enqueueApplicationScript:(NSData *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); RCTProfileBeginFlowEvent(); [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { RCTProfileEndFlowEvent(); RCTAssertJSThread(); if (scriptLoadError) { onComplete(scriptLoadError); return; } RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"FetchApplicationScriptCallbacks", nil); [self->_javaScriptExecutor flushedQueue:^(id json, NSError *error) { RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call,init"); [self handleBuffer:json batchEnded:YES]; onComplete(error); }]; }]; } #pragma mark - Payload Generation - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args { RCTAssertJSThread(); __weak __typeof(self) weakSelf = self; [_javaScriptExecutor callFunctionOnModule:module method:method arguments:args callback:^(id json, NSError *error) { [weakSelf _processResponse:json error:error]; }]; } - (void)_actuallyInvokeCallback:(NSNumber *)cbID arguments:(NSArray *)args { RCTAssertJSThread(); __weak __typeof(self) weakSelf = self; [_javaScriptExecutor invokeCallbackID:cbID arguments:args callback:^(id json, NSError *error) { [weakSelf _processResponse:json error:error]; }]; } - (void)_processResponse:(id)json error:(NSError *)error { if (error) { if ([error userInfo][RCTJSStackTraceKey]) { [self.redBox showErrorMessage:[error localizedDescription] withStack:[error userInfo][RCTJSStackTraceKey]]; } RCTFatal(error); } if (!_valid) { return; } [self handleBuffer:json batchEnded:YES]; } #pragma mark - Payload Processing - (void)handleBuffer:(id)buffer batchEnded:(BOOL)batchEnded { RCTAssertJSThread(); if (buffer != nil && buffer != (id)kCFNull) { _wasBatchActive = YES; [self handleBuffer:buffer]; [self partialBatchDidFlush]; } if (batchEnded) { if (_wasBatchActive) { [self batchDidComplete]; } _wasBatchActive = NO; } } - (void)handleBuffer:(NSArray *)buffer { NSArray *requestsArray = [RCTConvert NSArray:buffer]; if (RCT_DEBUG && requestsArray.count <= RCTBridgeFieldParams) { RCTLogError(@"Buffer should contain at least %tu sub-arrays. Only found %tu", RCTBridgeFieldParams + 1, requestsArray.count); return; } NSArray *moduleIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldRequestModuleIDs]]; NSArray *methodIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldMethodIDs]]; NSArray *paramsArrays = [RCTConvert NSArrayArray:requestsArray[RCTBridgeFieldParams]]; int64_t callID = -1; if (requestsArray.count > 3) { callID = [requestsArray[RCTBridgeFieldCallID] longLongValue]; } if (RCT_DEBUG && (moduleIDs.count != methodIDs.count || moduleIDs.count != paramsArrays.count)) { RCTLogError(@"Invalid data message - all must be length: %zd", moduleIDs.count); return; } NSMapTable *buckets = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:_moduleDataByName.count]; [moduleIDs enumerateObjectsUsingBlock:^(NSNumber *moduleID, NSUInteger i, __unused BOOL *stop) { RCTModuleData *moduleData = self->_moduleDataByID[moduleID.integerValue]; dispatch_queue_t queue = moduleData.methodQueue; NSMutableOrderedSet *set = [buckets objectForKey:queue]; if (!set) { set = [NSMutableOrderedSet new]; [buckets setObject:set forKey:queue]; } [set addObject:@(i)]; }]; for (dispatch_queue_t queue in buckets) { RCTProfileBeginFlowEvent(); dispatch_block_t block = ^{ RCTProfileEndFlowEvent(); NSOrderedSet *calls = [buckets objectForKey:queue]; RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTBatchedBridge handleBuffer:]", (@{ @"calls": @(calls.count), })); @autoreleasepool { for (NSNumber *indexObj in calls) { NSUInteger index = indexObj.unsignedIntegerValue; #if RCT_PROFILE if (RCT_DEV && callID != -1 && self->_flowIDMap != NULL && RCTProfileIsProfiling()) { [self.flowIDMapLock lock]; NSUInteger newFlowID = (NSUInteger)CFDictionaryGetValue(self->_flowIDMap, (const void *)(self->_flowID + index)); _RCTProfileEndFlowEvent(newFlowID); CFDictionaryRemoveValue(self->_flowIDMap, (const void *)(self->_flowID + index)); [self.flowIDMapLock unlock]; } #endif [self callNativeModule:[moduleIDs[index] integerValue] method:[methodIDs[index] integerValue] params:paramsArrays[index]]; } } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"objc_call,dispatch_async"); }; [self dispatchBlock:block queue:queue]; } _flowID = callID; } - (void)partialBatchDidFlush { for (RCTModuleData *moduleData in _moduleDataByID) { if (moduleData.hasInstance && moduleData.implementsPartialBatchDidFlush) { [self dispatchBlock:^{ [moduleData.instance partialBatchDidFlush]; } queue:moduleData.methodQueue]; } } } - (void)batchDidComplete { // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? for (RCTModuleData *moduleData in _moduleDataByID) { if (moduleData.hasInstance && moduleData.implementsBatchDidComplete) { [self dispatchBlock:^{ [moduleData.instance batchDidComplete]; } queue:moduleData.methodQueue]; } } } - (id)callNativeModule:(NSUInteger)moduleID method:(NSUInteger)methodID params:(NSArray *)params { if (!_valid) { return nil; } RCTModuleData *moduleData = _moduleDataByID[moduleID]; if (RCT_DEBUG && !moduleData) { RCTLogError(@"No module found for id '%zd'", moduleID); return nil; } id method = moduleData.methods[methodID]; if (RCT_DEBUG && !method) { RCTLogError(@"Unknown methodID: %zd for module: %zd (%@)", methodID, moduleID, moduleData.name); return nil; } @try { return [method invokeWithBridge:self module:moduleData.instance arguments:params]; } @catch (NSException *exception) { // Pass on JS exceptions if ([exception.name hasPrefix:RCTFatalExceptionName]) { @throw exception; } NSString *message = [NSString stringWithFormat: @"Exception '%@' was thrown while invoking %@ on target %@ with params %@", exception, method.JSMethodName, moduleData.name, params]; RCTFatal(RCTErrorWithMessage(message)); return nil; } } - (void)startProfiling { RCTAssertMainQueue(); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileInit(self); }]; } - (void)stopProfiling:(void (^)(NSData *))callback { RCTAssertMainQueue(); [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEnd(self, ^(NSString *log) { NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; callback(logData); }); }]; } - (BOOL)isBatchActive { return _wasBatchActive; } @end