react-native/React/CxxBridge/RCTCxxBridge.mm

1276 lines
43 KiB
Plaintext
Raw Normal View History

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <atomic>
#include <future>
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeMethod.h>
#import <React/RCTConvert.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTCxxModule.h>
#import <React/RCTCxxUtils.h>
#import <React/RCTDevSettings.h>
#import <React/RCTDisplayLink.h>
#import <React/RCTJavaScriptLoader.h>
#import <React/RCTLog.h>
#import <React/RCTModuleData.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTProfile.h>
#import <React/RCTRedBox.h>
#import <React/RCTUtils.h>
#import <React/RCTFollyConvert.h>
#import <cxxreact/CxxNativeModule.h>
#import <cxxreact/Instance.h>
#import <cxxreact/JSBundleType.h>
#import <cxxreact/JSCExecutor.h>
#import <cxxreact/JSIndexedRAMBundle.h>
#import <cxxreact/ModuleRegistry.h>
#import <cxxreact/Platform.h>
#import <cxxreact/RAMBundleRegistry.h>
#import <jschelpers/Value.h>
#import "NSDataBigString.h"
#import "RCTJSCHelpers.h"
#import "RCTMessageThread.h"
#import "RCTObjcExecutor.h"
#ifdef WITH_FBSYSTRACE
#import <React/RCTFBSystrace.h>
#endif
#if RCT_DEV && __has_include("RCTDevLoadingView.h")
#import "RCTDevLoadingView.h"
#endif
#define RCTAssertJSThread() \
RCTAssert(self.executorClass || self->_jsThread == [NSThread currentThread], \
@"This method must be called on JS thread")
static NSString *const RCTJSThreadName = @"com.facebook.react.JavaScript";
typedef void (^RCTPendingCall)();
using namespace facebook::react;
/**
* Must be kept in sync with `MessageQueue.js`.
*/
typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldRequestModuleIDs = 0,
RCTBridgeFieldMethodIDs,
RCTBridgeFieldParams,
RCTBridgeFieldCallID,
};
namespace {
class GetDescAdapter : public JSExecutorFactory {
public:
GetDescAdapter(RCTCxxBridge *bridge, std::shared_ptr<JSExecutorFactory> factory)
: bridge_(bridge)
, factory_(factory) {}
std::unique_ptr<JSExecutor> createJSExecutor(
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue) override {
auto ret = factory_->createJSExecutor(delegate, jsQueue);
bridge_.bridgeDescription =
[NSString stringWithFormat:@"RCTCxxBridge %s",
ret->getDescription().c_str()];
return std::move(ret);
}
private:
RCTCxxBridge *bridge_;
std::shared_ptr<JSExecutorFactory> factory_;
};
}
static bool isRAMBundle(NSData *script) {
BundleHeader header;
[script getBytes:&header length:sizeof(header)];
return parseTypeFromHeader(header) == ScriptTag::RAMBundle;
}
static void registerPerformanceLoggerHooks(RCTPerformanceLogger *performanceLogger) {
__weak RCTPerformanceLogger *weakPerformanceLogger = performanceLogger;
ReactMarker::logTaggedMarker = [weakPerformanceLogger](const ReactMarker::ReactMarkerId markerId, const char *tag) {
switch (markerId) {
case ReactMarker::RUN_JS_BUNDLE_START:
[weakPerformanceLogger markStartForTag:RCTPLScriptExecution];
break;
case ReactMarker::RUN_JS_BUNDLE_STOP:
[weakPerformanceLogger markStopForTag:RCTPLScriptExecution];
break;
case ReactMarker::NATIVE_REQUIRE_START:
[weakPerformanceLogger appendStartForTag:RCTPLRAMNativeRequires];
break;
case ReactMarker::NATIVE_REQUIRE_STOP:
[weakPerformanceLogger appendStopForTag:RCTPLRAMNativeRequires];
[weakPerformanceLogger addValue:1 forTag:RCTPLRAMNativeRequiresCount];
break;
case ReactMarker::CREATE_REACT_CONTEXT_STOP:
case ReactMarker::JS_BUNDLE_STRING_CONVERT_START:
case ReactMarker::JS_BUNDLE_STRING_CONVERT_STOP:
case ReactMarker::NATIVE_MODULE_SETUP_START:
case ReactMarker::NATIVE_MODULE_SETUP_STOP:
case ReactMarker::REGISTER_JS_SEGMENT_START:
case ReactMarker::REGISTER_JS_SEGMENT_STOP:
// These are not used on iOS.
break;
}
};
}
@interface RCTCxxBridge ()
@property (nonatomic, weak, readonly) RCTBridge *parentBridge;
@property (nonatomic, assign, readonly) BOOL moduleSetupComplete;
- (instancetype)initWithParentBridge:(RCTBridge *)bridge;
- (void)partialBatchDidFlush;
- (void)batchDidComplete;
@end
struct RCTInstanceCallback : public InstanceCallback {
__weak RCTCxxBridge *bridge_;
RCTInstanceCallback(RCTCxxBridge *bridge): bridge_(bridge) {};
void onBatchComplete() override {
// There's no interface to call this per partial batch
[bridge_ partialBatchDidFlush];
[bridge_ batchDidComplete];
}
};
@implementation RCTCxxBridge
{
BOOL _wasBatchActive;
BOOL _didInvalidate;
NSMutableArray<RCTPendingCall> *_pendingCalls;
std::atomic<NSInteger> _pendingCount;
// Native modules
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSMutableArray<RCTModuleData *> *_moduleDataByID;
NSMutableArray<Class> *_moduleClassesByID;
NSUInteger _modulesInitializedOnMainQueue;
RCTDisplayLink *_displayLink;
// JS thread management
NSThread *_jsThread;
std::shared_ptr<RCTMessageThread> _jsMessageThread;
// This is uniquely owned, but weak_ptr is used.
std::shared_ptr<Instance> _reactInstance;
}
@synthesize bridgeDescription = _bridgeDescription;
@synthesize loading = _loading;
@synthesize performanceLogger = _performanceLogger;
@synthesize valid = _valid;
+ (void)initialize
{
if (self == [RCTCxxBridge class]) {
RCTPrepareJSCExecutor();
}
}
- (JSGlobalContextRef)jsContextRef
{
return (JSGlobalContextRef)(_reactInstance ? _reactInstance->getJavaScriptContext() : nullptr);
}
- (std::shared_ptr<MessageQueueThread>)jsMessageThread
{
return _jsMessageThread;
}
- (BOOL)isInspectable
{
return _reactInstance ? _reactInstance->isInspectable() : NO;
}
- (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];
registerPerformanceLoggerHooks(_performanceLogger);
RCTLogInfo(@"Initializing %@ (parent: %@, executor: %@)", self, bridge, [self executorClass]);
/**
* Set Initial State
*/
_valid = YES;
_loading = YES;
_pendingCalls = [NSMutableArray new];
_displayLink = [RCTDisplayLink new];
_moduleDataByName = [NSMutableDictionary new];
_moduleClassesByID = [NSMutableArray new];
_moduleDataByID = [NSMutableArray new];
[RCTBridge setCurrentBridge:self];
}
return self;
}
+ (void)runRunLoop
{
@autoreleasepool {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);
// copy thread name to pthread name
pthread_setname_np([NSThread currentThread].name.UTF8String);
// Set up a dummy runloop source to avoid spinning
CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
CFRelease(noSpinSource);
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// run the run loop
while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
}
}
}
- (void)_tryAndHandleError:(dispatch_block_t)block
{
NSError *error = tryAndReturnError(block);
if (error) {
[self handleError:error];
}
}
/**
* Ensure block is run on the JS thread. If we're already on the JS thread, the block will execute synchronously.
* If we're not on the JS thread, the block is dispatched to that thread. Any errors encountered while executing
* the block will go through handleError:
*/
- (void)ensureOnJavaScriptThread:(dispatch_block_t)block
{
RCTAssert(_jsThread, @"This method must not be called before the JS thread is created");
// This does not use _jsMessageThread because it may be called early before the runloop reference is captured
// and _jsMessageThread is valid. _jsMessageThread also doesn't allow us to shortcut the dispatch if we're
// already on the correct thread.
if ([NSThread currentThread] == _jsThread) {
[self _tryAndHandleError:block];
} else {
[self performSelector:@selector(_tryAndHandleError:)
onThread:_jsThread
withObject:block
waitUntilDone:NO];
}
}
- (void)start
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge start]", nil);
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptWillStartLoadingNotification
object:_parentBridge userInfo:@{@"bridge": self}];
// Set up the JS thread early
_jsThread = [[NSThread alloc] initWithTarget:[self class]
selector:@selector(runRunLoop)
object:nil];
_jsThread.name = RCTJSThreadName;
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
#if RCT_DEBUG
_jsThread.stackSize *= 2;
#endif
[_jsThread start];
dispatch_group_t prepareBridge = dispatch_group_create();
[_performanceLogger markStartForTag:RCTPLNativeModuleInit];
[self registerExtraModules];
// Initialize all native modules that cannot be loaded lazily
(void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
[_performanceLogger markStopForTag:RCTPLNativeModuleInit];
// This doesn't really do anything. The real work happens in initializeBridge.
_reactInstance.reset(new Instance);
__weak RCTCxxBridge *weakSelf = self;
// Prepare executor factory (shared_ptr for copy into block)
std::shared_ptr<JSExecutorFactory> executorFactory;
if (!self.executorClass) {
if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>) self.delegate;
executorFactory = [cxxDelegate jsExecutorFactoryForBridge:self];
}
if (!executorFactory) {
BOOL useCustomJSC =
[self.delegate respondsToSelector:@selector(shouldBridgeUseCustomJSC:)] &&
[self.delegate shouldBridgeUseCustomJSC:self];
// We use the name of the device and the app for debugging & metrics
NSString *deviceName = [[UIDevice currentDevice] name];
NSString *appName = [[NSBundle mainBundle] bundleIdentifier];
// The arg is a cache dir. It's not used with standard JSC.
executorFactory.reset(new JSCExecutorFactory(folly::dynamic::object
("OwnerIdentity", "ReactNative")
("AppIdentity", [(appName ?: @"unknown") UTF8String])
("DeviceIdentity", [(deviceName ?: @"unknown") UTF8String])
("UseCustomJSC", (bool)useCustomJSC)
#if RCT_PROFILE
("StartSamplingProfilerOnInit", (bool)self.devSettings.startSamplingProfilerOnLaunch)
#endif
));
}
} else {
id<RCTJavaScriptExecutor> objcExecutor = [self moduleForClass:self.executorClass];
executorFactory.reset(new RCTObjcExecutorFactory(objcExecutor, ^(NSError *error) {
if (error) {
[weakSelf handleError:error];
}
}));
}
// Dispatch the instance initialization as soon as the initial module metadata has
// been collected (see initModules)
dispatch_group_enter(prepareBridge);
[self ensureOnJavaScriptThread:^{
[weakSelf _initializeBridge:executorFactory];
dispatch_group_leave(prepareBridge);
}];
// Load the source asynchronously, then store it for later execution.
dispatch_group_enter(prepareBridge);
__block NSData *sourceCode;
[self loadSource:^(NSError *error, RCTSource *source) {
if (error) {
[weakSelf handleError:error];
}
sourceCode = source.data;
dispatch_group_leave(prepareBridge);
} onProgress:^(RCTLoadingProgress *progressData) {
#if RCT_DEV && __has_include("RCTDevLoadingView.h")
RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]];
[loadingView updateProgress:progressData];
#endif
}];
// Wait for both the modules and source code to have finished loading
dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
RCTCxxBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
[strongSelf executeSourceCode:sourceCode sync:NO];
}
});
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:RCTBridgeWillDownloadScriptNotification object:_parentBridge];
[_performanceLogger markStartForTag:RCTPLScriptDownload];
NSUInteger cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil);
// Suppress a warning if RCTProfileBeginAsyncEvent gets compiled out
(void)cookie;
RCTPerformanceLogger *performanceLogger = _performanceLogger;
RCTSourceLoadBlock onSourceLoad = ^(NSError *error, RCTSource *source) {
RCTProfileEndAsyncEvent(0, @"native", cookie, @"JavaScript download", @"JS async");
[performanceLogger markStopForTag:RCTPLScriptDownload];
[performanceLogger setValue:source.length forTag:RCTPLBundleSize];
NSDictionary *userInfo = @{
RCTBridgeDidDownloadScriptNotificationSourceKey: source ?: [NSNull null],
RCTBridgeDidDownloadScriptNotificationBridgeDescriptionKey: self->_bridgeDescription ?: [NSNull null],
};
[center postNotificationName:RCTBridgeDidDownloadScriptNotification object:self->_parentBridge userInfo:userInfo];
_onSourceLoad(error, source);
};
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 if (!self.bundleURL) {
NSError *error = RCTErrorWithMessage(@"No bundle URL present.\n\nMake sure you're running a packager " \
"server or have included a .jsbundle file in your application bundle.");
onSourceLoad(error, nil);
} else {
[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, RCTSource *source) {
if (error) {
RCTLogError(@"Failed to load bundle(%@) with error:(%@ %@)", self.bundleURL, error.localizedDescription, error.localizedFailureReason);
return;
}
onSourceLoad(error, source);
}];
}
}
- (NSArray<Class> *)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;
}
- (id)jsBoundExtraModuleForClass:(Class)moduleClass
{
if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>) self.delegate;
if ([cxxDelegate respondsToSelector:@selector(jsBoundExtraModuleForClass:)]) {
return [cxxDelegate jsBoundExtraModuleForClass:moduleClass];
}
}
return nil;
}
- (std::shared_ptr<ModuleRegistry>)_buildModuleRegistry
{
if (!self.valid) {
return {};
}
[_performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge buildModuleRegistry]", nil);
__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<ModuleRegistry>(
createNativeModules(_moduleDataByID, self, _reactInstance),
moduleNotFoundCallback);
[_performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
return registry;
}
- (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
{
if (!self.valid) {
return;
}
RCTAssertJSThread();
__weak RCTCxxBridge *weakSelf = self;
_jsMessageThread = std::make_shared<RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
if (error) {
[weakSelf handleError:error];
}
});
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge initializeBridge:]", nil);
// This can only be false if the bridge was invalidated before startup completed
if (_reactInstance) {
#if RCT_DEV
executorFactory = std::make_shared<GetDescAdapter>(self, executorFactory);
#endif
// This is async, but any calls into JS are blocked by the m_syncReady CV in Instance
_reactInstance->initializeBridge(
std::make_unique<RCTInstanceCallback>(self),
executorFactory,
_jsMessageThread,
[self _buildModuleRegistry]);
#if RCT_PROFILE
if (RCTProfileIsProfiling()) {
_reactInstance->setGlobalVariable(
"__RCTProfileIsProfiling",
std::make_unique<JSBigStdString>("true"));
}
#endif
[self installExtraJSBinding];
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (NSArray<RCTModuleData *> *)registerModulesForClasses:(NSArray<Class> *)moduleClasses
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil);
NSArray *moduleClassesCopy = [moduleClasses copy];
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray arrayWithCapacity:moduleClassesCopy.count];
for (Class moduleClass in moduleClassesCopy) {
if (RCTJSINativeModuleEnabled() && [moduleClass conformsToProtocol:@protocol(RCTJSINativeModule)]) {
continue;
}
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 #13258411: can we defer this until config generation?
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self];
_moduleDataByName[moduleName] = moduleData;
[_moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
[_moduleDataByID addObjectsFromArray:moduleDataByID];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
return moduleDataByID;
}
- (void)registerExtraModules
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil);
NSArray<id<RCTBridgeModule>> *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:] preinitialized moduleData", nil);
// Set up moduleData for pre-initialized module instances
for (id<RCTBridgeModule> 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, @"");
}
- (void)installExtraJSBinding
{
if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>) self.delegate;
if ([cxxDelegate respondsToSelector:@selector(installExtraJSBinding:)]) {
[cxxDelegate installExtraJSBinding:self.jsContextRef];
}
}
}
- (NSArray<RCTModuleData *> *)_initializeModules:(NSArray<id<RCTBridgeModule>> *)modules
withDispatchGroup:(dispatch_group_t)dispatchGroup
lazilyDiscovered:(BOOL)lazilyDiscovered
{
RCTAssert(!(RCTIsMainQueue() && lazilyDiscovered), @"Lazy discovery can only happen off the Main Queue");
// Set up moduleData for automatically-exported modules
NSArray<RCTModuleData *> *moduleDataById = [self registerModulesForClasses:modules];
if (lazilyDiscovered) {
#if RCT_DEBUG
// 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);
}
#endif
} else {
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()) {
// Depends on moduleDataByID being loaded
RCTProfileHookModules(self);
}
#endif
return moduleDataById;
}
- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules
{
NSArray<RCTModuleData *> *newModules = [self _initializeModules:modules withDispatchGroup:NULL lazilyDiscovered:YES];
if (_reactInstance) {
_reactInstance->getModuleRegistry().registerModules(createNativeModules(newModules, self, _reactInstance));
}
}
- (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
{
RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTCxxBridge _prepareModulesWithDispatchGroup]", nil);
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;
}
// Set up modules that require main thread init or constants export
[_performanceLogger setValue:0 forTag:RCTPLNativeModuleMainThread];
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.requiresMainQueueSetup) {
// 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 && ![moduleData.moduleClass isSubclassOfClass:[RCTCxxModule class]]) {
[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)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData
{
[_displayLink registerModuleForFrameUpdates:module withModuleData:moduleData];
}
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync
{
// This will get called from whatever thread was actually executing JS.
dispatch_block_t completion = ^{
// Log start up metrics early before processing any other js calls
[self logStartupFinish];
// Flush pending calls immediately so we preserve ordering
[self _flushPendingCalls];
// Perform the state update and 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}];
// Starting the display link is not critical to startup, so do it last
[self ensureOnJavaScriptThread:^{
// Register the display link to start sending js calls after everything is setup
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
}];
});
};
if (sync) {
[self executeApplicationScriptSync:sourceCode url:self.bundleURL];
completion();
} else {
[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
}
#if RCT_DEV
Fix crash when loading local bundle and hot reload is enabled Summary: <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html Happy contributing! --> When initializing react native module using a url pointing to local jsbundle and hot reload is enabled, bridge, is trying to initialize the feature but crashes because there is no host in a local file url. 1. Create a new project. ```sh $ react-native init testPlanApp && cd testPlanApp ``` 2. Create jsbundle. ```sh $ react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/main.jsbundle ``` 3. Run the app and enable hot reload. 4. Enable airplane mode. 5. Close application and re-run. 6. See app crashes. <!-- Help reviewers and the release process by writing your own release notes **INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.** CATEGORY [----------] TYPE [ CLI ] [-------------] LOCATION [ DOCS ] [ BREAKING ] [-------------] [ GENERAl ] [ BUGFIX ] [-{Component}-] [ INTERNAL ] [ ENHANCEMENT ] [ {File} ] [ IOS ] [ FEATURE ] [ {Directory} ] |-----------| [ ANDROID ] [ MINOR ] [ {Framework} ] - | {Message} | [----------] [-------------] [-------------] |-----------| [CATEGORY] [TYPE] [LOCATION] - MESSAGE EXAMPLES: [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see --> [IOS] [BUGFIX] [Hot reloading] - Fix crash when loading local bundle and hot reload is enabled. Closes https://github.com/facebook/react-native/pull/16504 Differential Revision: D6133767 Pulled By: shergin fbshipit-source-id: 3f7668e3e8fa7e772f58420e5a8a0985a5f8e48b
2017-10-24 06:57:46 +00:00
if (self.devSettings.isHotLoadingAvailable && self.devSettings.isHotLoadingEnabled) {
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)handleError:(NSError *)error
{
// This is generally called when the infrastructure throws an
// exception while calling JS. Most product exceptions will not go
// through this method, but through RCTExceptionManager.
// There are three possible states:
// 1. initializing == _valid && _loading
// 2. initializing/loading finished (success or failure) == _valid && !_loading
// 3. invalidated == !_valid && !_loading
// !_valid && _loading can't happen.
// In state 1: on main queue, move to state 2, reset the bridge, and RCTFatal.
// In state 2: go directly to RCTFatal. Do not enqueue, do not collect $200.
// In state 3: do nothing.
if (self->_valid && !self->_loading) {
if ([error userInfo][RCTJSRawStackTraceKey]) {
[self.redBox showErrorMessage:[error localizedDescription]
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
}
RCTFatal(error);
// RN will stop, but let the rest of the app keep going.
return;
}
if (!_valid || !_loading) {
return;
}
// Hack: once the bridge is invalidated below, it won't initialize any new native
// modules. Initialize the redbox module now so we can still report this error.
RCTRedBox *redBox = [self redBox];
_loading = NO;
_valid = NO;
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_jsMessageThread) {
// Make sure initializeBridge completed
self->_jsMessageThread->runOnQueueSync([] {});
}
self->_reactInstance.reset();
self->_jsMessageThread.reset();
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptDidFailToLoadNotification
object:self->_parentBridge userInfo:@{@"bridge": self, @"error": error}];
if ([error userInfo][RCTJSRawStackTraceKey]) {
[redBox showErrorMessage:[error localizedDescription]
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
}
RCTFatal(error);
});
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(__unused id<RCTBridgeDelegate>)delegate
bundleURL:(__unused NSURL *)bundleURL
moduleProvider:(__unused RCTBridgeModuleListProvider)block
launchOptions:(__unused NSDictionary *)launchOptions)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL
moduleProvider:(__unused RCTBridgeModuleListProvider)block
launchOptions:(__unused NSDictionary *)launchOptions)
/**
* Prevent super from calling setUp (that'd create another batchedBridge)
*/
- (void)setUp {}
- (void)reload
{
if (!_valid) {
RCTLogError(@"Attempting to reload bridge before it's valid: %@. Try restarting the development server if connected.", self);
}
[_parentBridge reload];
}
- (Class)executorClass
{
return _parentBridge.executorClass;
}
- (void)setExecutorClass:(Class)executorClass
{
RCTAssertMainQueue();
_parentBridge.executorClass = executorClass;
}
- (NSURL *)bundleURL
{
return _parentBridge.bundleURL;
}
- (void)setBundleURL:(NSURL *)bundleURL
{
_parentBridge.bundleURL = bundleURL;
}
- (id<RCTBridgeDelegate>)delegate
{
return _parentBridge.delegate;
}
- (void)dispatchBlock:(dispatch_block_t)block
queue:(dispatch_queue_t)queue
{
if (queue == RCTJSThread) {
[self ensureOnJavaScriptThread:block];
} else if (queue) {
dispatch_async(queue, block);
}
}
#pragma mark - RCTInvalidating
- (void)invalidate
{
if (_didInvalidate) {
return;
}
RCTAssertMainQueue();
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]);
_loading = NO;
_valid = NO;
_didInvalidate = YES;
if ([RCTBridge currentBridge] == self) {
[RCTBridge setCurrentBridge:nil];
}
// Stop JS instance and message thread
[self ensureOnJavaScriptThread:^{
[self->_displayLink invalidate];
self->_displayLink = nil;
if (RCTProfileIsProfiling()) {
RCTProfileUnhookModules(self);
}
// Invalidate modules
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module
// wrappers) or we may have invalid pointers still in flight.
dispatch_group_t moduleInvalidation = dispatch_group_create();
for (RCTModuleData *moduleData in self->_moduleDataByID) {
// Be careful when grabbing an instance here, we don't want to instantiate
// any modules just to invalidate them.
if (![moduleData hasInstance]) {
continue;
}
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
dispatch_group_enter(moduleInvalidation);
[self dispatchBlock:^{
[(id<RCTInvalidating>)moduleData.instance invalidate];
dispatch_group_leave(moduleInvalidation);
} queue:moduleData.methodQueue];
}
[moduleData invalidate];
}
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
RCTLogError(@"Timed out waiting for modules to be invalidated");
}
self->_reactInstance.reset();
self->_jsMessageThread.reset();
self->_moduleDataByName = nil;
self->_moduleDataByID = nil;
self->_moduleClassesByID = nil;
self->_pendingCalls = nil;
[self->_jsThread cancel];
self->_jsThread = nil;
CFRunLoopStop(CFRunLoopGetCurrent());
}];
}
- (void)logMessage:(NSString *)message level:(NSString *)level
{
if (RCT_DEBUG && _valid) {
[self enqueueJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
args:@[level, message]
completion:NULL];
}
}
#pragma mark - RCTBridge methods
- (void)_runAfterLoad:(RCTPendingCall)block
{
// Ordering here is tricky. Ideally, the C++ bridge would provide
// functionality to defer calls until after the app is loaded. Until that
// happens, we do this. _pendingCount keeps a count of blocks which have
// been deferred. It is incremented using an atomic barrier call before each
// block is added to the js queue, and decremented using an atomic barrier
// call after the block is executed. If _pendingCount is zero, there is no
// work either in the js queue, or in _pendingCalls, so it is safe to add new
// work to the JS queue directly.
if (self.loading || _pendingCount > 0) {
// From the callers' perspecive:
// Phase 1: jsQueueBlocks are added to the queue; _pendingCount is
// incremented for each. If the first block is created after self.loading is
// true, phase 1 will be nothing.
_pendingCount++;
dispatch_block_t jsQueueBlock = ^{
// From the perspective of the JS queue:
if (self.loading) {
// Phase A: jsQueueBlocks are executed. self.loading is true, so they
// are added to _pendingCalls.
[self->_pendingCalls addObject:block];
} else {
// Phase C: More jsQueueBlocks are executed. self.loading is false, so
// each block is executed, adding work to the queue, and _pendingCount is
// decremented.
block();
self->_pendingCount--;
}
};
[self ensureOnJavaScriptThread:jsQueueBlock];
} else {
// Phase 2/Phase D: blocks are executed directly, adding work to the JS queue.
block();
}
}
- (void)logStartupFinish
{
// Log metrics about native requires during the bridge startup.
uint64_t nativeRequiresCount = [_performanceLogger valueForTag:RCTPLRAMNativeRequiresCount];
[_performanceLogger setValue:nativeRequiresCount forTag:RCTPLRAMStartupNativeRequiresCount];
uint64_t nativeRequires = [_performanceLogger valueForTag:RCTPLRAMNativeRequires];
[_performanceLogger setValue:nativeRequires forTag:RCTPLRAMStartupNativeRequires];
[_performanceLogger markStopForTag:RCTPLBridgeStartup];
}
- (void)_flushPendingCalls
{
RCT_PROFILE_BEGIN_EVENT(0, @"Processing pendingCalls", @{ @"count": [@(_pendingCalls.count) stringValue] });
// Phase B: _flushPendingCalls happens. Each block in _pendingCalls is
// executed, adding work to the queue, and _pendingCount is decremented.
// loading is set to NO.
NSArray<RCTPendingCall> *pendingCalls = _pendingCalls;
_pendingCalls = nil;
for (RCTPendingCall call in pendingCalls) {
call();
_pendingCount--;
}
_loading = NO;
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
/**
* Public. Can be invoked from any thread.
*/
- (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion
{
if (!self.valid) {
return;
}
/**
* AnyThread
*/
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge enqueueJSCall:]", nil);
RCTProfileBeginFlowEvent();
__weak __typeof(self) weakSelf = self;
[self _runAfterLoad:^(){
RCTProfileEndFlowEvent();
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf->_reactInstance) {
strongSelf->_reactInstance->callJSFunction([module UTF8String], [method UTF8String],
convertIdToFollyDynamic(args ?: @[]));
// ensureOnJavaScriptThread may execute immediately, so use jsMessageThread, to make sure
// the block is invoked after callJSFunction
if (completion) {
if (strongSelf->_jsMessageThread) {
strongSelf->_jsMessageThread->runOnQueue(completion);
} else {
RCTLogWarn(@"Can't invoke completion without messageThread");
}
}
}
}];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
/**
* Called by RCTModuleMethod from any thread.
*/
- (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args
{
if (!self.valid) {
return;
}
/**
* AnyThread
*/
RCTProfileBeginFlowEvent();
__weak __typeof(self) weakSelf = self;
[self _runAfterLoad:^(){
RCTProfileEndFlowEvent();
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf->_reactInstance) {
strongSelf->_reactInstance->callJSCallback([cbID unsignedLongLongValue], convertIdToFollyDynamic(args ?: @[]));
}
}];
}
/**
* Private hack to support `setTimeout(fn, 0)`
*/
- (void)_immediatelyCallTimer:(NSNumber *)timer
{
RCTAssertJSThread();
if (_reactInstance) {
_reactInstance->callJSFunction("JSTimers", "callTimers",
folly::dynamic::array(folly::dynamic::array([timer doubleValue])));
}
}
- (void)enqueueApplicationScript:(NSData *)script
url:(NSURL *)url
onComplete:(dispatch_block_t)onComplete
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge enqueueApplicationScript]", nil);
[self executeApplicationScript:script url:url async:YES];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// Assumes that onComplete can be called when the next block on the JS thread is scheduled
if (onComplete) {
RCTAssert(_jsMessageThread != nullptr, @"Cannot invoke completion without jsMessageThread");
_jsMessageThread->runOnQueue(onComplete);
}
}
- (void)executeApplicationScriptSync:(NSData *)script url:(NSURL *)url
{
[self executeApplicationScript:script url:url async:NO];
}
- (void)executeApplicationScript:(NSData *)script
url:(NSURL *)url
async:(BOOL)async
{
[self _tryAndHandleError:^{
NSString *sourceUrlStr = deriveSourceURL(url);
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptWillStartExecutingNotification
object:self->_parentBridge userInfo:@{@"bridge": self}];
if (isRAMBundle(script)) {
[self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad];
auto ramBundle = std::make_unique<JSIndexedRAMBundle>(sourceUrlStr.UTF8String);
std::unique_ptr<const JSBigString> scriptStr = ramBundle->getStartupCode();
[self->_performanceLogger markStopForTag:RCTPLRAMBundleLoad];
[self->_performanceLogger setValue:scriptStr->size() forTag:RCTPLRAMStartupCodeSize];
if (self->_reactInstance) {
auto registry = RAMBundleRegistry::multipleBundlesRegistry(std::move(ramBundle), JSIndexedRAMBundle::buildFactory());
self->_reactInstance->loadRAMBundle(std::move(registry), std::move(scriptStr),
sourceUrlStr.UTF8String, !async);
}
} else if (self->_reactInstance) {
self->_reactInstance->loadScriptFromString(std::make_unique<NSDataBigString>(script),
sourceUrlStr.UTF8String, !async);
} else {
std::string methodName = async ? "loadApplicationScript" : "loadApplicationScriptSync";
throw std::logic_error("Attempt to call " + methodName + ": on uninitialized bridge");
}
}];
}
- (void)registerSegmentWithId:(NSUInteger)segmentId path:(NSString *)path
{
if (_reactInstance) {
_reactInstance->registerBundle(static_cast<uint32_t>(segmentId), path.UTF8String);
}
}
#pragma mark - Payload Processing
- (void)partialBatchDidFlush
{
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.implementsPartialBatchDidFlush) {
[self dispatchBlock:^{
[moduleData.instance partialBatchDidFlush];
} queue:moduleData.methodQueue];
}
}
}
- (void)batchDidComplete
{
// TODO #12592471: batchDidComplete is only used by RCTUIManager,
// can we eliminate this special case?
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.implementsBatchDidComplete) {
[self dispatchBlock:^{
[moduleData.instance batchDidComplete];
} queue:moduleData.methodQueue];
}
}
}
- (void)startProfiling
{
RCTAssertMainQueue();
[self ensureOnJavaScriptThread:^{
#if WITH_FBSYSTRACE
[RCTFBSystrace registerCallbacks];
#endif
RCTProfileInit(self);
[self enqueueJSCall:@"Systrace" method:@"setEnabled" args:@[@YES] completion:NULL];
}];
}
- (void)stopProfiling:(void (^)(NSData *))callback
{
RCTAssertMainQueue();
[self ensureOnJavaScriptThread:^{
[self enqueueJSCall:@"Systrace" method:@"setEnabled" args:@[@NO] completion:NULL];
RCTProfileEnd(self, ^(NSString *log) {
NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding];
callback(logData);
#if WITH_FBSYSTRACE
[RCTFBSystrace unregisterCallbacks];
#endif
});
}];
}
- (BOOL)isBatchActive
{
return _wasBatchActive;
}
@end