Customize main thread initialization of native modules

Reviewed By: mhorowitz

Differential Revision: D3580025

fbshipit-source-id: 18abc15b894c745f584d91da47035c6db036aafa
This commit is contained in:
Pieter De Baets 2016-08-02 10:08:59 -07:00 committed by Facebook Github Bot 6
parent 6ae348e8de
commit 47d6d289d4
6 changed files with 150 additions and 40 deletions

View File

@ -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<any> | 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;

View File

@ -46,9 +46,10 @@ RCT_EXTERN NSArray<Class> *RCTGetModuleClasses(void);
{
BOOL _wasBatchActive;
NSMutableArray<dispatch_block_t> *_pendingCalls;
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSArray<RCTModuleData *> *_moduleDataByID;
NSArray<Class> *_moduleClassesByID;
NSUInteger _modulesInitializedOnMainQueue;
RCTDisplayLink *_displayLink;
}
@ -99,6 +100,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<RCTBridgeDelegate>)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<RCTBridgeDelegate>)dele
[strongSelf executeSourceCode:sourceCode];
}
});
RCT_PROFILE_END_EVENT(0, @"", nil);
}
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad
@ -233,6 +240,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<RCTBridgeDelegate>)dele
- (void)initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
{
RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge initModules]", nil);
[_performanceLogger markStartForTag:RCTPLNativeModuleInit];
NSArray<id<RCTBridgeModule>> *extraModules = nil;
@ -252,7 +267,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<RCTBridgeDelegate>)dele
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
// Set up moduleData for pre-initialized module instances
RCT_PROFILE_BEGIN_EVENT(0, @"extraModules", nil);
for (id<RCTBridgeModule> module in extraModules) {
Class moduleClass = [module class];
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
@ -317,11 +332,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)dele
_javaScriptExecutor = (id<RCTJavaScriptExecutor>)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<RCTJavaScriptExecutor> executorModule = [self.executorClass new];
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:executorModule
@ -333,8 +350,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<RCTBridgeDelegate>)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,79 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(id<RCTBridgeDelegate>)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<Class> *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(shouldBridgeInitializeNativeModulesOnCurrentThread:)]) {
initializeImmediately = [self.delegate shouldBridgeInitializeNativeModulesOnCurrentThread: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
{
[self prepareModulesWithDispatchGroup:NULL];
}
- (void)setUpExecutor
@ -594,23 +655,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);
@ -700,11 +760,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 ?: @[]];
@ -712,7 +772,6 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
completion();
}
} queue:RCTJSThread];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"", nil);
}
@ -786,7 +845,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
@ -800,7 +859,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) {
@ -926,7 +985,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];
@ -938,7 +997,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];

View File

@ -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 instantiate any (whitelisted) native modules
* that require main thread initialization, so we don't depend on dispatch_sync
* later on.
*
* 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.
*/

View File

@ -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

View File

@ -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 <NSObject>
/**
@ -46,7 +47,28 @@
* 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<id<RCTBridgeModule>> *)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 subset 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<Class> *)whitelistedModulesForBridge:(RCTBridge *)bridge;
/**
* When initializing main-thread-only native modules, the bridge will be default
* 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.
*
* @experimental
*/
- (BOOL)shouldBridgeInitializeNativeModulesOnCurrentThread:(RCTBridge *)bridge;
/**
* The bridge will automatically attempt to load the JS source code from the

View File

@ -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()) {
RCTLogError(@"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()) {
RCTLogError(@"Required dispatch_sync to load constants for %@. This may lead to deadlocks", _moduleClass);
}
RCTExecuteOnMainThread(^{
self->_constantsToExport = [self->_instance constantsToExport] ?: @{};
}, YES);