mirror of
https://github.com/status-im/react-native.git
synced 2025-01-09 17:15:54 +00:00
17df595e32
Summary: public Android implement ViewManager methods via a dispatch method on UIManager, whereas iOS implements them by exposing the methods on the view manager modules directly. This diff polyfills Android's implementation on top of the iOS implementation, allowing the same JS API to be used for both. Reviewed By: javache Differential Revision: D2803020 fb-gh-sync-id: 0da0544e593dc936467d16ce957a77f7ca41355b
1019 lines
29 KiB
Objective-C
1019 lines
29 KiB
Objective-C
/**
|
|
* 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 <Foundation/Foundation.h>
|
|
|
|
#import "RCTAssert.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTBridge+Private.h"
|
|
#import "RCTBridgeMethod.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTJSCExecutor.h"
|
|
#import "RCTFrameUpdate.h"
|
|
#import "RCTJavaScriptLoader.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTModuleData.h"
|
|
#import "RCTPerformanceLogger.h"
|
|
#import "RCTProfile.h"
|
|
#import "RCTSourceCode.h"
|
|
#import "RCTUtils.h"
|
|
|
|
#define RCTAssertJSThread() \
|
|
RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTJSCExecutor"] || \
|
|
[[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \
|
|
@"This method must be called on JS thread")
|
|
|
|
/**
|
|
* Must be kept in sync with `MessageQueue.js`.
|
|
*/
|
|
typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
|
|
RCTBridgeFieldRequestModuleIDs = 0,
|
|
RCTBridgeFieldMethodIDs,
|
|
RCTBridgeFieldParamss,
|
|
};
|
|
|
|
RCT_EXTERN NSArray<Class> *RCTGetModuleClasses(void);
|
|
|
|
@interface RCTBatchedBridge : RCTBridge
|
|
|
|
@property (nonatomic, weak) RCTBridge *parentBridge;
|
|
|
|
@end
|
|
|
|
@implementation RCTBatchedBridge
|
|
{
|
|
BOOL _loading;
|
|
BOOL _valid;
|
|
BOOL _wasBatchActive;
|
|
__weak id<RCTJavaScriptExecutor> _javaScriptExecutor;
|
|
NSMutableArray<dispatch_block_t> *_pendingCalls;
|
|
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
|
|
NSArray<RCTModuleData *> *_moduleDataByID;
|
|
NSDictionary<NSString *, id<RCTBridgeModule>> *_modulesByName_DEPRECATED;
|
|
NSArray<Class> *_moduleClassesByID;
|
|
CADisplayLink *_jsDisplayLink;
|
|
NSMutableSet<RCTModuleData *> *_frameUpdateObservers;
|
|
|
|
// Bridge startup stats (TODO: capture in perf logger)
|
|
NSUInteger _syncInitializedModules;
|
|
NSUInteger _asyncInitializedModules;
|
|
}
|
|
|
|
- (instancetype)initWithParentBridge:(RCTBridge *)bridge
|
|
{
|
|
RCTAssertMainThread();
|
|
RCTAssertParam(bridge);
|
|
|
|
if ((self = [super initWithBundleURL:bridge.bundleURL
|
|
moduleProvider:bridge.moduleProvider
|
|
launchOptions:bridge.launchOptions])) {
|
|
|
|
_parentBridge = bridge;
|
|
|
|
/**
|
|
* Set Initial State
|
|
*/
|
|
_valid = YES;
|
|
_loading = YES;
|
|
_pendingCalls = [NSMutableArray new];
|
|
_frameUpdateObservers = [NSMutableSet new];
|
|
_jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)];
|
|
|
|
[RCTBridge setCurrentBridge:self];
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:RCTJavaScriptWillStartLoadingNotification
|
|
object:_parentBridge userInfo:@{@"bridge": self}];
|
|
|
|
[self start];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)start
|
|
{
|
|
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) {
|
|
if (error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[weakSelf stopLoadingWithError:error];
|
|
});
|
|
}
|
|
|
|
sourceCode = source;
|
|
dispatch_group_leave(initModulesAndLoadSource);
|
|
}];
|
|
|
|
// Synchronously initialize all native modules that cannot be loaded lazily
|
|
[self initModules];
|
|
|
|
#if RCT_DEBUG
|
|
_syncInitializedModules = [[_moduleDataByID valueForKeyPath:@"@sum.hasInstance"] integerValue];
|
|
#endif
|
|
|
|
if (RCTProfileIsProfiling()) {
|
|
// Depends on moduleDataByID being loaded
|
|
RCTProfileHookModules(self);
|
|
}
|
|
|
|
__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, ^{
|
|
[weakSelf setUpExecutor];
|
|
});
|
|
|
|
// Asynchronously gather the module config
|
|
dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{
|
|
if (weakSelf.isValid) {
|
|
|
|
RCTPerformanceLoggerStart(RCTPLNativeModulePrepareConfig);
|
|
config = [weakSelf moduleConfig];
|
|
RCTPerformanceLoggerEnd(RCTPLNativeModulePrepareConfig);
|
|
|
|
#if RCT_DEBUG
|
|
NSInteger total = [[_moduleDataByID valueForKeyPath:@"@sum.hasInstance"] integerValue];
|
|
_asyncInitializedModules = total - _syncInitializedModules;
|
|
#endif
|
|
|
|
}
|
|
});
|
|
|
|
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.
|
|
RCTPerformanceLoggerStart(RCTPLNativeModuleInjectConfig);
|
|
[weakSelf injectJSONConfiguration:config onComplete:^(NSError *error) {
|
|
RCTPerformanceLoggerEnd(RCTPLNativeModuleInjectConfig);
|
|
if (error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[weakSelf stopLoadingWithError:error];
|
|
});
|
|
}
|
|
}];
|
|
dispatch_group_leave(initModulesAndLoadSource);
|
|
});
|
|
});
|
|
|
|
dispatch_group_notify(initModulesAndLoadSource, dispatch_get_main_queue(), ^{
|
|
RCTBatchedBridge *strongSelf = weakSelf;
|
|
if (sourceCode && strongSelf.loading) {
|
|
dispatch_async(bridgeQueue, ^{
|
|
[weakSelf executeSourceCode:sourceCode];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad
|
|
{
|
|
RCTPerformanceLoggerStart(RCTPLScriptDownload);
|
|
NSUInteger cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil);
|
|
|
|
// Suppress a warning if RCTProfileBeginAsyncEvent gets compiled out
|
|
(void)cookie;
|
|
|
|
RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSData *source) {
|
|
RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil);
|
|
RCTPerformanceLoggerEnd(RCTPLScriptDownload);
|
|
|
|
_onSourceLoad(error, source);
|
|
};
|
|
|
|
if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) {
|
|
[self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad];
|
|
} else if (self.bundleURL) {
|
|
[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad];
|
|
} else {
|
|
// Allow testing without a script
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self didFinishLoading];
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:RCTJavaScriptDidLoadNotification
|
|
object:_parentBridge userInfo:@{@"bridge": self}];
|
|
});
|
|
onSourceLoad(nil, nil);
|
|
}
|
|
}
|
|
|
|
- (NSArray<Class> *)moduleClasses
|
|
{
|
|
if (RCT_DEBUG && self.isValid && _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
|
|
{
|
|
RCTModuleData *moduleData = _moduleDataByName[moduleName];
|
|
return moduleData.instance;
|
|
}
|
|
|
|
- (NSArray *)configForModuleName:(NSString *)moduleName
|
|
{
|
|
RCTModuleData *moduleData = _moduleDataByName[moduleName];
|
|
if (!moduleData) {
|
|
moduleData = _moduleDataByName[[@"RCT" stringByAppendingString:moduleName]];
|
|
}
|
|
if (moduleData) {
|
|
return moduleData.config;
|
|
}
|
|
return (id)kCFNull;
|
|
}
|
|
|
|
- (void)initModules
|
|
{
|
|
RCTAssertMainThread();
|
|
RCTPerformanceLoggerStart(RCTPLNativeModuleInit);
|
|
|
|
// Register passed-in module instances
|
|
NSMutableDictionary *preregisteredModules = [NSMutableDictionary new];
|
|
|
|
NSArray<id<RCTBridgeModule>> *extraModules = nil;
|
|
if (self.delegate) {
|
|
if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) {
|
|
extraModules = [self.delegate extraModulesForBridge:_parentBridge];
|
|
}
|
|
} else if (self.moduleProvider) {
|
|
extraModules = self.moduleProvider();
|
|
}
|
|
|
|
for (id<RCTBridgeModule> module in extraModules) {
|
|
preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module;
|
|
}
|
|
|
|
SEL setBridgeSelector = NSSelectorFromString(@"setBridge:");
|
|
IMP objectInitMethod = [NSObject instanceMethodForSelector:@selector(init)];
|
|
|
|
// Set up moduleData and pre-initialize module instances
|
|
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
|
|
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
|
|
for (Class moduleClass in RCTGetModuleClasses()) {
|
|
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
|
|
id module = preregisteredModules[moduleName];
|
|
if (!module) {
|
|
// Check if the module class, or any of its superclasses override init
|
|
// or setBridge:. If they do, we assume that they are expecting to be
|
|
// initialized when the bridge first loads.
|
|
if ([moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod ||
|
|
[moduleClass instancesRespondToSelector:setBridgeSelector]) {
|
|
module = [moduleClass new];
|
|
if (!module) {
|
|
module = (id)kCFNull;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for module name collisions.
|
|
// It's OK to have a name collision as long as the second instance is null.
|
|
if (module != (id)kCFNull && moduleDataByName[moduleName] && !preregisteredModules[moduleName]) {
|
|
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the name "
|
|
"'%@', but name was already registered by class %@", moduleClass,
|
|
moduleName, moduleDataByName[moduleName].moduleClass);
|
|
}
|
|
|
|
// Instantiate moduleData (TODO: defer this until config generation)
|
|
RCTModuleData *moduleData;
|
|
if (module) {
|
|
if (module != (id)kCFNull) {
|
|
moduleData = [[RCTModuleData alloc] initWithModuleInstance:module
|
|
bridge:self];
|
|
}
|
|
} else {
|
|
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
|
|
bridge:self];
|
|
}
|
|
if (moduleData) {
|
|
moduleDataByName[moduleName] = moduleData;
|
|
[moduleDataByID addObject:moduleData];
|
|
}
|
|
}
|
|
|
|
// Store modules
|
|
_moduleDataByID = [moduleDataByID copy];
|
|
_moduleDataByName = [moduleDataByName copy];
|
|
_moduleClassesByID = [moduleDataByID valueForKey:@"moduleClass"];
|
|
|
|
/**
|
|
* The executor is a bridge module, wait for it to be created and set it before
|
|
* any other module has access to the bridge
|
|
*/
|
|
_javaScriptExecutor = [self moduleForClass:self.executorClass];
|
|
|
|
for (RCTModuleData *moduleData in _moduleDataByID) {
|
|
if (moduleData.hasInstance) {
|
|
[moduleData setBridgeForInstance];
|
|
}
|
|
}
|
|
|
|
for (RCTModuleData *moduleData in _moduleDataByID) {
|
|
if (moduleData.hasInstance) {
|
|
[moduleData methodQueue]; // initialize the queue
|
|
}
|
|
}
|
|
|
|
for (RCTModuleData *moduleData in _moduleDataByID) {
|
|
if (moduleData.hasInstance) {
|
|
[self registerModuleForFrameUpdates:moduleData.instance
|
|
withModuleData:moduleData];
|
|
}
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:RCTDidCreateNativeModules
|
|
object:self userInfo:@{@"bridge": self}];
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
RCTPerformanceLoggerEnd(RCTPLNativeModuleInit);
|
|
}
|
|
|
|
- (void)setUpExecutor
|
|
{
|
|
[_javaScriptExecutor setUp];
|
|
}
|
|
|
|
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
|
|
withModuleData:(RCTModuleData *)moduleData
|
|
{
|
|
if (![_frameUpdateObservers containsObject:moduleData]) {
|
|
if ([moduleData.moduleClass conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) {
|
|
[_frameUpdateObservers addObject:moduleData];
|
|
// Don't access the module instance via moduleData, as this will cause deadlock
|
|
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)module;
|
|
__weak typeof(self) weakSelf = self;
|
|
__weak typeof(_javaScriptExecutor) weakJavaScriptExecutor = _javaScriptExecutor;
|
|
observer.pauseCallback = ^{
|
|
[weakJavaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
[weakSelf updateJSDisplayLinkState];
|
|
}];
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSString *)moduleConfig
|
|
{
|
|
NSMutableArray<NSArray *> *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)updateJSDisplayLinkState
|
|
{
|
|
RCTAssertJSThread();
|
|
|
|
BOOL pauseDisplayLink = YES;
|
|
for (RCTModuleData *moduleData in _frameUpdateObservers) {
|
|
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)moduleData.instance;
|
|
if (!observer.paused) {
|
|
pauseDisplayLink = NO;
|
|
break;
|
|
}
|
|
}
|
|
_jsDisplayLink.paused = pauseDisplayLink;
|
|
}
|
|
|
|
- (void)injectJSONConfiguration:(NSString *)configJSON
|
|
onComplete:(void (^)(NSError *))onComplete
|
|
{
|
|
if (!self.valid) {
|
|
return;
|
|
}
|
|
|
|
[_javaScriptExecutor injectJSONText:configJSON
|
|
asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
|
|
callback:onComplete];
|
|
}
|
|
|
|
- (void)executeSourceCode:(NSData *)sourceCode
|
|
{
|
|
if (!self.valid || !_javaScriptExecutor) {
|
|
return;
|
|
}
|
|
|
|
RCTSourceCode *sourceCodeModule = [self moduleForClass:[RCTSourceCode class]];
|
|
sourceCodeModule.scriptURL = self.bundleURL;
|
|
sourceCodeModule.scriptData = sourceCode;
|
|
|
|
[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) {
|
|
if (!self.isValid) {
|
|
return;
|
|
}
|
|
|
|
if (loadError) {
|
|
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 = [_javaScriptExecutor isKindOfClass:[RCTJSCExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop];
|
|
[_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes];
|
|
|
|
// 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(), ^{
|
|
[self didFinishLoading];
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:RCTJavaScriptDidLoadNotification
|
|
object:_parentBridge userInfo:@{@"bridge": self}];
|
|
});
|
|
}];
|
|
|
|
#if RCT_DEV
|
|
|
|
if (RCTGetURLQueryParam(self.bundleURL, @"hot")) {
|
|
NSString *path = [self.bundleURL.path substringFromIndex:1]; // strip initial slash
|
|
[self enqueueJSCall:@"HMRClient.enable" args:@[@"ios", path]];
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
- (void)didFinishLoading
|
|
{
|
|
_loading = NO;
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
for (dispatch_block_t call in _pendingCalls) {
|
|
call();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)stopLoadingWithError:(NSError *)error
|
|
{
|
|
RCTAssertMainThread();
|
|
|
|
if (!self.isValid || !self.loading) {
|
|
return;
|
|
}
|
|
|
|
_loading = NO;
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName:RCTJavaScriptDidFailToLoadNotification
|
|
object:_parentBridge userInfo:@{@"bridge": self, @"error": error}];
|
|
|
|
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];
|
|
}
|
|
|
|
- (Class)executorClass
|
|
{
|
|
return _parentBridge.executorClass ?: [RCTJSCExecutor class];
|
|
}
|
|
|
|
- (void)setExecutorClass:(Class)executorClass
|
|
{
|
|
RCTAssertMainThread();
|
|
|
|
_parentBridge.executorClass = executorClass;
|
|
}
|
|
|
|
- (NSURL *)bundleURL
|
|
{
|
|
return _parentBridge.bundleURL;
|
|
}
|
|
|
|
- (void)setBundleURL:(NSURL *)bundleURL
|
|
{
|
|
_parentBridge.bundleURL = bundleURL;
|
|
}
|
|
|
|
- (id<RCTBridgeDelegate>)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) {
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
|
|
} else if (queue) {
|
|
dispatch_async(queue, block);
|
|
}
|
|
}
|
|
|
|
#pragma mark - RCTInvalidating
|
|
|
|
- (void)invalidate
|
|
{
|
|
if (!self.valid) {
|
|
return;
|
|
}
|
|
|
|
RCTAssertMainThread();
|
|
|
|
_loading = NO;
|
|
_valid = NO;
|
|
if ([RCTBridge currentBridge] == self) {
|
|
[RCTBridge setCurrentBridge:nil];
|
|
}
|
|
|
|
// Invalidate modules
|
|
dispatch_group_t group = dispatch_group_create();
|
|
for (RCTModuleData *moduleData in _moduleDataByName.allValues) {
|
|
if (moduleData.instance == _javaScriptExecutor) {
|
|
continue;
|
|
}
|
|
|
|
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
|
|
dispatch_group_enter(group);
|
|
[self dispatchBlock:^{
|
|
[(id<RCTInvalidating>)moduleData.instance invalidate];
|
|
dispatch_group_leave(group);
|
|
} queue:moduleData.methodQueue];
|
|
}
|
|
[moduleData invalidate];
|
|
}
|
|
|
|
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
[_jsDisplayLink invalidate];
|
|
_jsDisplayLink = nil;
|
|
|
|
[_javaScriptExecutor invalidate];
|
|
_javaScriptExecutor = nil;
|
|
|
|
if (RCTProfileIsProfiling()) {
|
|
RCTProfileUnhookModules(self);
|
|
}
|
|
_moduleDataByName = nil;
|
|
_moduleDataByID = nil;
|
|
_moduleClassesByID = nil;
|
|
_modulesByName_DEPRECATED = nil;
|
|
_frameUpdateObservers = nil;
|
|
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)logMessage:(NSString *)message level:(NSString *)level
|
|
{
|
|
if (RCT_DEBUG && [_javaScriptExecutor isValid]) {
|
|
[self enqueueJSCall:@"RCTLog.logIfNoNativeHook"
|
|
args:@[level, message]];
|
|
}
|
|
}
|
|
|
|
#pragma mark - RCTBridge methods
|
|
|
|
/**
|
|
* Public. Can be invoked from any thread.
|
|
*/
|
|
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
|
|
{
|
|
/**
|
|
* AnyThread
|
|
*/
|
|
|
|
NSArray<NSString *> *ids = [moduleDotMethod componentsSeparatedByString:@"."];
|
|
|
|
NSString *module = ids[0];
|
|
NSString *method = ids[1];
|
|
|
|
RCTProfileBeginFlowEvent();
|
|
|
|
__weak RCTBatchedBridge *weakSelf = self;
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
RCTProfileEndFlowEvent();
|
|
|
|
RCTBatchedBridge *strongSelf = weakSelf;
|
|
if (!strongSelf || !strongSelf.valid) {
|
|
return;
|
|
}
|
|
|
|
if (strongSelf.loading) {
|
|
dispatch_block_t pendingCall = ^{
|
|
[weakSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]];
|
|
};
|
|
[strongSelf->_pendingCalls addObject:pendingCall];
|
|
} else {
|
|
[strongSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]];
|
|
}
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Called by RCTModuleMethod from any thread.
|
|
*/
|
|
- (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args
|
|
{
|
|
/**
|
|
* AnyThread
|
|
*/
|
|
|
|
RCTProfileBeginFlowEvent();
|
|
|
|
__weak RCTBatchedBridge *weakSelf = self;
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
RCTProfileEndFlowEvent();
|
|
|
|
RCTBatchedBridge *strongSelf = weakSelf;
|
|
if (!strongSelf || !strongSelf.valid) {
|
|
return;
|
|
}
|
|
|
|
if (strongSelf.loading) {
|
|
dispatch_block_t pendingCall = ^{
|
|
[weakSelf _actuallyInvokeCallback:cbID arguments:args ?: @[]];
|
|
};
|
|
[strongSelf->_pendingCalls addObject:pendingCall];
|
|
} else {
|
|
[strongSelf _actuallyInvokeCallback:cbID arguments:args];
|
|
}
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Private hack to support `setTimeout(fn, 0)`
|
|
*/
|
|
- (void)_immediatelyCallTimer:(NSNumber *)timer
|
|
{
|
|
RCTAssertJSThread();
|
|
|
|
dispatch_block_t block = ^{
|
|
[self _actuallyInvokeAndProcessModule:@"JSTimersExecution"
|
|
method:@"callTimers"
|
|
arguments:@[@[timer]]];
|
|
};
|
|
|
|
if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) {
|
|
[_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block];
|
|
} else {
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
|
|
}
|
|
}
|
|
|
|
- (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(0, @"FetchApplicationScriptCallbacks", nil);
|
|
[_javaScriptExecutor flushedQueue:^(id json, NSError *error)
|
|
{
|
|
RCT_PROFILE_END_EVENT(0, @"js_call,init", @{
|
|
@"json": RCTNullIfNil(json),
|
|
@"error": RCTNullIfNil(error),
|
|
});
|
|
|
|
[self handleBuffer:json batchEnded:YES];
|
|
|
|
onComplete(error);
|
|
}];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Payload Generation
|
|
|
|
- (void)_actuallyInvokeAndProcessModule:(NSString *)module
|
|
method:(NSString *)method
|
|
arguments:(NSArray *)args
|
|
{
|
|
RCTAssertJSThread();
|
|
|
|
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
|
|
if (error) {
|
|
RCTFatal(error);
|
|
}
|
|
|
|
if (!self.isValid) {
|
|
return;
|
|
}
|
|
[self handleBuffer:json batchEnded:YES];
|
|
};
|
|
|
|
[_javaScriptExecutor callFunctionOnModule:module
|
|
method:method
|
|
arguments:args
|
|
callback:processResponse];
|
|
}
|
|
|
|
- (void)_actuallyInvokeCallback:(NSNumber *)cbID
|
|
arguments:(NSArray *)args
|
|
{
|
|
RCTAssertJSThread();
|
|
|
|
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
|
|
if (error) {
|
|
RCTFatal(error);
|
|
}
|
|
|
|
if (!self.isValid) {
|
|
return;
|
|
}
|
|
[self handleBuffer:json batchEnded:YES];
|
|
};
|
|
|
|
[_javaScriptExecutor invokeCallbackID:cbID
|
|
arguments:args
|
|
callback:processResponse];
|
|
}
|
|
|
|
#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 <= RCTBridgeFieldParamss) {
|
|
RCTLogError(@"Buffer should contain at least %tu sub-arrays. Only found %tu",
|
|
RCTBridgeFieldParamss + 1, requestsArray.count);
|
|
return;
|
|
}
|
|
|
|
NSArray<NSNumber *> *moduleIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldRequestModuleIDs]];
|
|
NSArray<NSNumber *> *methodIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldMethodIDs]];
|
|
NSArray<NSArray *> *paramsArrays = [RCTConvert NSArrayArray:requestsArray[RCTBridgeFieldParamss]];
|
|
|
|
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 = _moduleDataByID[moduleID.integerValue];
|
|
dispatch_queue_t queue = moduleData.methodQueue;
|
|
NSMutableOrderedSet<NSNumber *> *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();
|
|
|
|
RCT_PROFILE_BEGIN_EVENT(0, RCTCurrentThreadName(), nil);
|
|
|
|
NSOrderedSet *calls = [buckets objectForKey:queue];
|
|
@autoreleasepool {
|
|
for (NSNumber *indexObj in calls) {
|
|
NSUInteger index = indexObj.unsignedIntegerValue;
|
|
[self _handleRequestNumber:index
|
|
moduleID:[moduleIDs[index] integerValue]
|
|
methodID:[methodIDs[index] integerValue]
|
|
params:paramsArrays[index]];
|
|
}
|
|
}
|
|
|
|
RCT_PROFILE_END_EVENT(0, @"objc_call,dispatch_async", @{
|
|
@"calls": @(calls.count),
|
|
});
|
|
};
|
|
|
|
if (queue == RCTJSThread) {
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
|
|
} else if (queue) {
|
|
dispatch_async(queue, block);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)partialBatchDidFlush
|
|
{
|
|
for (RCTModuleData *moduleData in _moduleDataByID) {
|
|
if (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.implementsBatchDidComplete) {
|
|
[self dispatchBlock:^{
|
|
[moduleData.instance batchDidComplete];
|
|
} queue:moduleData.methodQueue];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)_handleRequestNumber:(NSUInteger)i
|
|
moduleID:(NSUInteger)moduleID
|
|
methodID:(NSUInteger)methodID
|
|
params:(NSArray *)params
|
|
{
|
|
if (!self.isValid) {
|
|
return NO;
|
|
}
|
|
|
|
if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) {
|
|
RCTLogError(@"Invalid module/method/params tuple for request #%zd", i);
|
|
return NO;
|
|
}
|
|
|
|
RCTModuleData *moduleData = _moduleDataByID[moduleID];
|
|
if (RCT_DEBUG && !moduleData) {
|
|
RCTLogError(@"No module found for id '%zd'", moduleID);
|
|
return NO;
|
|
}
|
|
|
|
id<RCTBridgeMethod> method = moduleData.methods[methodID];
|
|
if (RCT_DEBUG && !method) {
|
|
RCTLogError(@"Unknown methodID: %zd for module: %zd (%@)", methodID, moduleID, moduleData.name);
|
|
return NO;
|
|
}
|
|
|
|
@try {
|
|
[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 YES;
|
|
}
|
|
|
|
- (void)_jsThreadUpdate:(CADisplayLink *)displayLink
|
|
{
|
|
RCTAssertJSThread();
|
|
RCT_PROFILE_BEGIN_EVENT(0, @"DispatchFrameUpdate", nil);
|
|
|
|
RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
|
|
for (RCTModuleData *moduleData in _frameUpdateObservers) {
|
|
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)moduleData.instance;
|
|
if (!observer.paused) {
|
|
RCTProfileBeginFlowEvent();
|
|
|
|
[self dispatchBlock:^{
|
|
RCTProfileEndFlowEvent();
|
|
[observer didUpdateFrame:frameUpdate];
|
|
} queue:moduleData.methodQueue];
|
|
}
|
|
}
|
|
|
|
[self updateJSDisplayLinkState];
|
|
|
|
|
|
RCTProfileImmediateEvent(0, @"JS Thread Tick", displayLink.timestamp, 'g');
|
|
|
|
RCT_PROFILE_END_EVENT(0, @"objc_call", nil);
|
|
}
|
|
|
|
- (void)startProfiling
|
|
{
|
|
RCTAssertMainThread();
|
|
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
RCTProfileInit(self);
|
|
}];
|
|
}
|
|
|
|
- (void)stopProfiling:(void (^)(NSData *))callback
|
|
{
|
|
RCTAssertMainThread();
|
|
|
|
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
|
RCTProfileEnd(self, ^(NSString *log) {
|
|
NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding];
|
|
callback(logData);
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (BOOL)isBatchActive
|
|
{
|
|
return _wasBatchActive;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBatchedBridge(Deprecated)
|
|
|
|
- (NSDictionary *)modules
|
|
{
|
|
if (!_modulesByName_DEPRECATED) {
|
|
// Check classes are set up
|
|
[self moduleClasses];
|
|
NSMutableDictionary *modulesByName = [NSMutableDictionary new];
|
|
for (NSString *moduleName in _moduleDataByName) {
|
|
id module = [self moduleForName:moduleName];
|
|
if (module) {
|
|
modulesByName[moduleName] = module;
|
|
}
|
|
};
|
|
_modulesByName_DEPRECATED = [modulesByName copy];
|
|
}
|
|
return _modulesByName_DEPRECATED;
|
|
}
|
|
|
|
@end
|