react-native/ReactKit/Base/RCTBridge.m

507 lines
17 KiB
Mathematica
Raw Normal View History

2015-01-30 01:10:49 +00:00
// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTBridge.h"
#import <objc/message.h>
#import "RCTModuleMethod.h"
#import "RCTInvalidating.h"
#import "RCTEventDispatcher.h"
2015-01-30 01:10:49 +00:00
#import "RCTLog.h"
#import "RCTModuleIDs.h"
#import "RCTUtils.h"
/**
* Functions are the one thing that aren't automatically converted to OBJC
* blocks, according to this revert: http://trac.webkit.org/changeset/144489
* They must be expressed as `JSValue`s.
*
* But storing callbacks causes reference cycles!
* http://stackoverflow.com/questions/19202248/how-can-i-use-jsmanagedvalue-to-avoid-a-reference-cycle-without-the-jsvalue-gett
* We'll live with the leak for now, but need to clean this up asap:
* Passing a reference to the `context` to the bridge would make it easy to
* execute JS. We can add `JSManagedValue`s to protect against this. The same
* needs to be done in `RCTTiming` and friends.
*/
/**
* Must be kept in sync with `MessageQueue.js`.
*/
typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldRequestModuleIDs = 0,
RCTBridgeFieldMethodIDs,
RCTBridgeFieldParamss,
RCTBridgeFieldResponseCBIDs,
RCTBridgeFieldResponseReturnValues,
RCTBridgeFieldFlushDateMillis
};
static NSString *RCTModuleName(Class moduleClass)
2015-01-30 01:10:49 +00:00
{
if ([moduleClass respondsToSelector:@selector(moduleName)]) {
return [moduleClass moduleName];
} else {
// Default implementation, works in most cases
return NSStringFromClass(moduleClass);
2015-01-30 01:10:49 +00:00
}
}
static NSDictionary *RCTNativeModuleClasses(void)
2015-01-30 01:10:49 +00:00
{
static NSMutableDictionary *modules;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
modules = [NSMutableDictionary dictionary];
unsigned int classCount;
Class *classes = objc_copyClassList(&classCount);
for (unsigned int i = 0; i < classCount; i++) {
Class cls = classes[i];
if (!class_getSuperclass(cls)) {
// Class has no superclass - it's probably something weird
continue;
}
if (![cls conformsToProtocol:@protocol(RCTNativeModule)]) {
// Not an RCTNativeModule
continue;
}
// Get module name
NSString *moduleName = RCTModuleName(cls);
// Check module name is unique
id existingClass = modules[moduleName];
RCTCAssert(existingClass == Nil, @"Attempted to register RCTNativeModule class %@ for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass);
modules[moduleName] = cls;
}
free(classes);
});
return modules;
}
@implementation RCTBridge
{
NSMutableDictionary *_moduleInstances;
NSDictionary *_javaScriptModulesConfig;
id<RCTJavaScriptExecutor> _javaScriptExecutor;
}
static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (instancetype)initWithJavaScriptExecutor:(id<RCTJavaScriptExecutor>)javaScriptExecutor
javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig
{
if ((self = [super init])) {
_javaScriptExecutor = javaScriptExecutor;
_latestJSExecutor = _javaScriptExecutor;
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
2015-01-30 01:10:49 +00:00
_javaScriptModulesConfig = javaScriptModulesConfig;
_shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL);
// Register modules
_moduleInstances = [[NSMutableDictionary alloc] init];
2015-01-30 01:10:49 +00:00
[RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) {
if (_moduleInstances[moduleName] == nil) {
if ([moduleClass instancesRespondToSelector:@selector(initWithBridge:)]) {
_moduleInstances[moduleName] = [[moduleClass alloc] initWithBridge:self];
} else {
_moduleInstances[moduleName] = [[moduleClass alloc] init];
}
2015-01-30 01:10:49 +00:00
}
}];
[self doneRegisteringModules];
}
return self;
}
- (void)dealloc
{
RCTAssert(!self.valid, @"must call -invalidate before -dealloc; TODO: why not call it here then?");
}
#pragma mark - RCTInvalidating
- (BOOL)isValid
{
return _javaScriptExecutor != nil;
}
- (void)invalidate
{
if (_latestJSExecutor == _javaScriptExecutor) {
_latestJSExecutor = nil;
}
_javaScriptExecutor = nil;
dispatch_sync(_shadowQueue, ^{
// Make sure all dispatchers have been executed before continuing
// TODO: is this still needed?
2015-01-30 01:10:49 +00:00
});
2015-01-30 01:10:49 +00:00
for (id target in _moduleInstances.objectEnumerator) {
if ([target respondsToSelector:@selector(invalidate)]) {
[(id<RCTInvalidating>)target invalidate];
}
}
[_moduleInstances removeAllObjects];
}
/**
* - TODO (#5906496): When we build a `MessageQueue.m`, handling all the requests could
* cause both a queue of "responses". We would flush them here. However, we
* currently just expect each objc block to handle its own response sending
* using a `RCTResponseSenderBlock`.
*/
#pragma mark - RCTBridge methods
/**
* Like JS::call, for objective-c.
*/
- (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args
{
RCTAssertMainThread();
[self _invokeRemoteJSModule:moduleID methodID:methodID args:args];
}
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
if (scriptLoadError) {
onComplete(scriptLoadError);
return;
}
[_javaScriptExecutor executeJSCall:@"BatchedBridge"
method:@"flushedQueue"
arguments:@[]
callback:^(id objcValue, NSError *error) {
[self _handleBuffer:objcValue];
onComplete(error);
}];
}];
}
#pragma mark - Payload Generation
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
{
NSTimeInterval startJS = RCTTGetAbsoluteTime();
RCTJavaScriptCallback processResponse = ^(id objcValue, NSError *error) {
NSTimeInterval startNative = RCTTGetAbsoluteTime();
[self _handleBuffer:objcValue];
NSTimeInterval end = RCTTGetAbsoluteTime();
NSTimeInterval timeJS = startNative - startJS;
NSTimeInterval timeNative = end - startNative;
// TODO: surface this performance information somewhere
[[NSNotificationCenter defaultCenter] postNotificationName:@"PERF" object:nil userInfo:@{@"JS": @(timeJS * 1000000), @"Native": @(timeNative * 1000000)}];
};
[_javaScriptExecutor executeJSCall:module
method:method
arguments:args
callback:processResponse];
}
- (void)_invokeRemoteJSModule:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args
{
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[@(moduleID), @(methodID), args]];
}
/**
* TODO (#5906496): Have responses piggy backed on a round trip with ObjC->JS requests.
*/
- (void)_sendResponseToJavaScriptCallbackID:(NSInteger)cbID args:(NSArray *)args
{
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[@(cbID), args]];
}
#pragma mark - Payload Processing
- (void)_handleBuffer:(id)buffer
{
if (buffer == nil || buffer == (id)kCFNull) {
return;
}
if (![buffer isKindOfClass:[NSArray class]]) {
RCTLogMustFix(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class]));
return;
}
NSArray *requestsArray = (NSArray *)buffer;
NSUInteger bufferRowCount = [requestsArray count];
NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1;
if (bufferRowCount != expectedFieldsCount) {
RCTLogMustFix(@"Must pass all fields to buffer - expected %zd, saw %zd", expectedFieldsCount, bufferRowCount);
return;
}
for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) {
id field = [requestsArray objectAtIndex:fieldIndex];
if (![field isKindOfClass:[NSArray class]]) {
RCTLogMustFix(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class]));
return;
}
}
NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs];
NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs];
NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss];
2015-01-30 01:10:49 +00:00
NSUInteger numRequests = [moduleIDs count];
BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramsArrays count];
2015-01-30 01:10:49 +00:00
if (!allSame) {
RCTLogMustFix(@"Invalid data message - all must be length: %zd", numRequests);
return;
}
for (NSUInteger i = 0; i < numRequests; i++) {
@autoreleasepool {
[self _handleRequestNumber:i
moduleID:[moduleIDs[i] integerValue]
methodID:[methodIDs[i] integerValue]
params:paramsArrays[i]];
2015-01-30 01:10:49 +00:00
}
}
// TODO: only used by RCTUIManager - can we eliminate this special case?
dispatch_async(_shadowQueue, ^{
for (id target in _moduleInstances.objectEnumerator) {
if ([target respondsToSelector:@selector(batchDidComplete)]) {
2015-01-30 01:10:49 +00:00
[target batchDidComplete];
}
2015-01-30 01:10:49 +00:00
}
});
2015-01-30 01:10:49 +00:00
}
- (BOOL)_handleRequestNumber:(NSUInteger)i
moduleID:(NSInteger)moduleID
methodID:(NSInteger)methodID
params:(NSArray *)params
2015-01-30 01:10:49 +00:00
{
if (![params isKindOfClass:[NSArray class]]) {
2015-01-30 01:10:49 +00:00
RCTLogMustFix(@"Invalid module/method/params tuple for request #%zd", i);
return NO;
2015-01-30 01:10:49 +00:00
}
2015-01-30 01:10:49 +00:00
if (moduleID < 0 || moduleID >= RCTExportedMethodsByModule().count) {
return NO;
}
2015-01-30 01:10:49 +00:00
NSString *moduleName = RCTExportedModuleNameAtSortedIndex(moduleID);
NSArray *methods = RCTExportedMethodsByModule()[moduleName];
if (methodID < 0 || methodID >= methods.count) {
return NO;
}
2015-01-30 01:10:49 +00:00
RCTModuleMethod *method = methods[methodID];
NSUInteger methodArity = method.arity;
if (params.count != methodArity) {
RCTLogMustFix(@"Expected %tu arguments but got %tu invoking %@.%@",
methodArity,
params.count,
moduleName,
method.JSMethodName);
2015-01-30 01:10:49 +00:00
return NO;
}
2015-01-30 01:10:49 +00:00
__weak RCTBridge *weakSelf = self;
dispatch_async(_shadowQueue, ^{
__strong RCTBridge *strongSelf = weakSelf;
2015-01-30 01:10:49 +00:00
if (!strongSelf.isValid) {
// strongSelf has been invalidated since the dispatch_async call and this
// invocation should not continue.
return;
}
2015-01-30 01:10:49 +00:00
NSInvocation *invocation = [RCTBridge invocationForAdditionalArguments:methodArity];
// TODO: we should just store module instances by index, since that's how we look them up anyway
id target = strongSelf->_moduleInstances[moduleName];
RCTAssert(target != nil, @"No module found for name '%@'", moduleName);
[invocation setArgument:&target atIndex:0];
2015-01-30 01:10:49 +00:00
SEL selector = method.selector;
[invocation setArgument:&selector atIndex:1];
2015-01-30 01:10:49 +00:00
// Retain used blocks until after invocation completes.
NSMutableArray *blocks = [NSMutableArray array];
2015-01-30 01:10:49 +00:00
[params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) {
if ([param isEqual:[NSNull null]]) {
param = nil;
} else if ([method.blockArgumentIndexes containsIndex:idx]) {
id block = [strongSelf createResponseSenderBlock:[param integerValue]];
[blocks addObject:block];
param = block;
}
2015-01-30 01:10:49 +00:00
[invocation setArgument:&param atIndex:idx + 2];
}];
2015-01-30 01:10:49 +00:00
@try {
[invocation invoke];
}
@catch (NSException *exception) {
RCTLogMustFix(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, target, params, exception);
}
@finally {
// Force `blocks` to remain alive until here.
blocks = nil;
}
});
2015-01-30 01:10:49 +00:00
return YES;
}
/**
* Returns a callback that reports values back to the JS thread.
* TODO (#5906496): These responses should go into their own queue `MessageQueue.m` that
* mirrors the JS queue and protocol. For now, we speak the "language" of the JS
* queue by packing it into an array that matches the wire protocol.
*/
- (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)cbID
{
if (!cbID) {
return nil;
}
return ^(NSArray *args) {
[self _sendResponseToJavaScriptCallbackID:cbID args:args];
};
}
+ (NSInvocation *)invocationForAdditionalArguments:(NSUInteger)argCount
{
static NSMutableDictionary *invocations;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
invocations = [NSMutableDictionary dictionary];
});
id key = @(argCount);
NSInvocation *invocation = invocations[key];
if (invocation == nil) {
NSString *objCTypes = [@"v@:" stringByPaddingToLength:3 + argCount withString:@"@" startingAtIndex:0];
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:objCTypes.UTF8String];
invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocations[key] = invocation;
}
return invocation;
}
2015-01-30 01:10:49 +00:00
- (void)doneRegisteringModules
{
RCTAssertMainThread();
RCTAssert(_javaScriptModulesConfig != nil, @"JS module config not loaded in APP");
NSMutableDictionary *objectsToInject = [NSMutableDictionary dictionary];
// Dictionary of { moduleName0: { moduleID: 0, methods: { methodName0: { methodID: 0, type: remote }, methodName1: { ... }, ... }, ... }
NSUInteger moduleCount = RCTExportedMethodsByModule().count;
NSMutableDictionary *moduleConfigs = [NSMutableDictionary dictionaryWithCapacity:RCTExportedMethodsByModule().count];
for (NSUInteger i = 0; i < moduleCount; i++) {
NSString *moduleName = RCTExportedModuleNameAtSortedIndex(i);
NSArray *rawMethods = RCTExportedMethodsByModule()[moduleName];
NSMutableDictionary *methods = [NSMutableDictionary dictionaryWithCapacity:rawMethods.count];
[rawMethods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *stop) {
methods[method.JSMethodName] = @{
@"methodID": @(methodID),
@"type": @"remote",
};
}];
NSMutableDictionary *moduleConfig = [NSMutableDictionary dictionary];
moduleConfig[@"moduleID"] = @(i);
moduleConfig[@"methods"] = methods;
id target = [_moduleInstances objectForKey:moduleName];
if ([target respondsToSelector:@selector(constantsToExport)]) {
2015-01-30 01:10:49 +00:00
moduleConfig[@"constants"] = [target constantsToExport];
}
moduleConfigs[moduleName] = moduleConfig;
}
NSDictionary *batchedBridgeConfig = @{
@"remoteModuleConfig": moduleConfigs,
@"localModulesConfig": _javaScriptModulesConfig
};
NSString *configJSON = RCTJSONStringify(batchedBridgeConfig, NULL);
objectsToInject[@"__fbBatchedBridgeConfig"] = configJSON;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *script, BOOL *stop) {
[_javaScriptExecutor injectJSONText:script asGlobalObjectNamed:objectName callback:^(id err) {
dispatch_semaphore_signal(semaphore);
}];
}];
for (NSUInteger i = 0, count = objectsToInject.count; i < count; i++) {
if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) {
RCTLogMustFix(@"JavaScriptExecutor take too long to inject JSON object");
}
}
}
- (void)registerRootView:(RCTRootView *)rootView
{
// TODO: only used by RCTUIManager - can we eliminate this special case?
for (id target in _moduleInstances.objectEnumerator) {
if ([target respondsToSelector:@selector(registerRootView:)]) {
[target registerRootView:rootView];
}
}
}
2015-01-30 01:10:49 +00:00
+ (BOOL)hasValidJSExecutor
{
return (_latestJSExecutor != nil && [_latestJSExecutor isValid]);
}
+ (void)log:(NSArray *)objects level:(NSString *)level
{
if (!_latestJSExecutor || ![_latestJSExecutor isValid]) {
RCTLogError(@"%@", RCTLogFormatString(@"ERROR: No valid JS executor to log %@.", objects));
return;
}
NSMutableArray *args = [NSMutableArray arrayWithObject:level];
// TODO (#5906496): Find out and document why we skip the first object
for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) {
if ([NSJSONSerialization isValidJSONObject:@[ob]]) {
[args addObject:ob];
} else {
[args addObject:[ob description]];
}
}
// Note the js executor could get invalidated while we're trying to call this...need to watch out for that.
[_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
arguments:args
callback:^(id objcValue, NSError *error) {}];
}
@end