react-native/ReactKit/Base/RCTBridge.m

648 lines
21 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 "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 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 = [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls);
2015-01-30 01:10:49 +00:00
// 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;
NSMutableDictionary *_moduleIDLookup;
NSMutableDictionary *_methodIDLookup;
2015-01-30 01:10:49 +00:00
id<RCTJavaScriptExecutor> _javaScriptExecutor;
}
static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (instancetype)initWithJavaScriptExecutor:(id<RCTJavaScriptExecutor>)javaScriptExecutor
{
if ((self = [super init])) {
_javaScriptExecutor = javaScriptExecutor;
_latestJSExecutor = _javaScriptExecutor;
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
_shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL);
// Instantiate 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
}
}];
_moduleIDLookup = [[NSMutableDictionary alloc] init];
_methodIDLookup = [[NSMutableDictionary 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:(NSString *)moduleDotMethod args:(NSArray *)args
2015-01-30 01:10:49 +00:00
{
NSNumber *moduleID = _moduleIDLookup[moduleDotMethod];
RCTAssert(moduleID, @"Module '%@' not registered.",
[[moduleDotMethod componentsSeparatedByString:@"."] firstObject]);
NSNumber *methodID = _methodIDLookup[moduleDotMethod];
RCTAssert(methodID, @"Method '%@' not registered.", moduleDotMethod);
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, args]];
2015-01-30 01:10:49 +00:00
}
- (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];
}
/**
* 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
// 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);
SEL selector = method.selector;
NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setArgument:&target atIndex:0];
2015-01-30 01:10:49 +00:00
[invocation setArgument:&selector atIndex:1];
2015-01-30 01:10:49 +00:00
// Retain used blocks until after invocation completes.
NS_VALID_UNTIL_END_OF_SCOPE 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;
}
NSUInteger argIdx = idx + 2;
BOOL shouldSet = YES;
const char *argumentType = [methodSignature getArgumentTypeAtIndex:argIdx];
switch (argumentType[0]) {
case ':':
if ([param isKindOfClass:[NSString class]]) {
SEL selector = NSSelectorFromString(param);
[invocation setArgument:&selector atIndex:argIdx];
shouldSet = NO;
}
break;
case '*':
if ([param isKindOfClass:[NSString class]]) {
const char *string = [param UTF8String];
[invocation setArgument:&string atIndex:argIdx];
shouldSet = NO;
}
break;
#define CASE(_value, _type, _selector) \
case _value: \
if ([param respondsToSelector:@selector(_selector)]) { \
_type value = [param _selector]; \
[invocation setArgument:&value atIndex:argIdx]; \
shouldSet = NO; \
} \
break;
CASE('c', char, charValue)
CASE('C', unsigned char, unsignedCharValue)
CASE('s', short, shortValue)
CASE('S', unsigned short, unsignedShortValue)
CASE('i', int, intValue)
CASE('I', unsigned int, unsignedIntValue)
CASE('l', long, longValue)
CASE('L', unsigned long, unsignedLongValue)
CASE('q', long long, longLongValue)
CASE('Q', unsigned long long, unsignedLongLongValue)
CASE('f', float, floatValue)
CASE('d', double, doubleValue)
CASE('B', BOOL, boolValue)
default:
break;
}
if (shouldSet) {
[invocation setArgument:&param atIndex:argIdx];
}
2015-01-30 01:10:49 +00:00
}];
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);
}
});
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;
}
- (NSArray *)JSMethods
{
NSMutableArray *methods = [[NSMutableArray alloc] init];
// Add globally used methods
[methods addObjectsFromArray:@[
@"Bundler.runApplication",
@"RCTEventEmitter.receiveEvent",
@"RCTEventEmitter.receiveTouches",
]];
// NOTE: these methods are currently unused in the OSS project
// @"Dimensions.set",
// @"RCTDeviceEventEmitter.emit",
// @"RCTNativeAppEventEmitter.emit",
// @"ReactIOS.unmountComponentAtNodeAndRemoveContainer",
// Register individual methods from modules
for (Class cls in RCTNativeModuleClasses().allValues) {
if (RCTClassOverridesClassMethod(cls, @selector(JSMethods))) {
[methods addObjectsFromArray:[cls JSMethods]];
}
}
return methods;
}
2015-01-30 01:10:49 +00:00
- (void)doneRegisteringModules
{
// TODO: everything in this method can actually be determined
// statically from just the class, and can therefore be cached
// in a dispatch_once instead of being repeated every time we
// reload or create a new RootView.
2015-01-30 01:10:49 +00:00
RCTAssertMainThread();
/**
* This constructs the remote modules configuration data structure,
* which represents the native modules and methods that will be called
* by JS. A numeric ID is assigned to each module and method, which will
* be used to communicate via the bridge. The structure of each
* module is as follows:
*
* "ModuleName1": {
* "moduleID": 0,
* "methods": {
* "methodName1": {
* "methodID": 0,
* "type": "remote"
* },
* "methodName2": {
* "methodID": 1,
* "type": "remote"
* },
* etc...
* },
* "constants": {
* ...
* }
* },
* etc...
*/
2015-01-30 01:10:49 +00:00
NSUInteger moduleCount = RCTExportedMethodsByModule().count;
NSMutableDictionary *remoteModules = [NSMutableDictionary dictionaryWithCapacity:RCTExportedMethodsByModule().count];
2015-01-30 01:10:49 +00:00
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",
};
}];
NSDictionary *module = @{
@"moduleID": @(i),
@"methods": methods
};
2015-01-30 01:10:49 +00:00
id target = _moduleInstances[moduleName];
if (RCTClassOverridesClassMethod([target class], @selector(constantsToExport))) {
module = [module mutableCopy];
((NSMutableDictionary *)module)[@"constants"] = [[target class] constantsToExport];
2015-01-30 01:10:49 +00:00
}
remoteModules[moduleName] = module;
2015-01-30 01:10:49 +00:00
}
/**
* As above, but for local modules/methods, which represent JS classes
* and methods that will be called by the native code via the bridge.
* Structure is essentially the same as for remote modules:
*
* "ModuleName1": {
* "moduleID": 0,
* "methods": {
* "methodName1": {
* "methodID": 0,
* "type": "local"
* },
* "methodName2": {
* "methodID": 1,
* "type": "local"
* },
* etc...
* }
* },
* etc...
*/
NSMutableDictionary *localModules = [[NSMutableDictionary alloc] init];
for (NSString *moduleDotMethod in [self JSMethods]) {
NSArray *parts = [moduleDotMethod componentsSeparatedByString:@"."];
RCTAssert(parts.count == 2, @"'%@' is not a valid JS method definition - expected 'Module.method' format.", moduleDotMethod);
// Add module if it doesn't already exist
NSString *moduleName = parts[0];
NSDictionary *module = localModules[moduleName];
if (!module) {
module = @{
@"moduleID": @(localModules.count),
@"methods": [[NSMutableDictionary alloc] init]
};
localModules[moduleName] = module;
}
// Add method if it doesn't already exist
NSString *methodName = parts[1];
NSMutableDictionary *methods = module[@"methods"];
if (!methods[methodName]) {
methods[methodName] = @{
@"methodID": @(methods.count),
@"type": @"local"
};
}
// Add module and method lookup
_moduleIDLookup[moduleDotMethod] = module[@"moduleID"];
_methodIDLookup[moduleDotMethod] = methods[methodName][@"methodID"];
}
/**
* Inject module data into JS context
*/
NSString *configJSON = RCTJSONStringify(@{
@"remoteModuleConfig": remoteModules,
@"localModulesConfig": localModules
}, NULL);
2015-01-30 01:10:49 +00:00
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) {
dispatch_semaphore_signal(semaphore);
2015-01-30 01:10:49 +00:00
}];
if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) {
RCTLogMustFix(@"JavaScriptExecutor take too long to inject JSON object");
2015-01-30 01:10:49 +00:00
}
}
- (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