diff --git a/Libraries/AdSupport/RCTAdSupport.h b/Libraries/AdSupport/RCTAdSupport.h index d55d503a1..56e561f74 100644 --- a/Libraries/AdSupport/RCTAdSupport.h +++ b/Libraries/AdSupport/RCTAdSupport.h @@ -7,8 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import - #import "RCTBridgeModule.h" @interface RCTAdSupport : NSObject diff --git a/Libraries/AdSupport/RCTAdSupport.m b/Libraries/AdSupport/RCTAdSupport.m index 1b2ad92de..dff794e34 100644 --- a/Libraries/AdSupport/RCTAdSupport.m +++ b/Libraries/AdSupport/RCTAdSupport.m @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTAdSupport.h" @implementation RCTAdSupport diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 13c3f079a..cff4ece15 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -217,6 +217,11 @@ RCT_EXPORT_METHOD(startAnimation:(NSNumber *)reactTag } NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:keypath]; +#if !CGFLOAT_IS_DOUBLE + if ([fromValue isKindOfClass:[NSNumber class]]) { + fromValue = [NSNumber numberWithFloat:[(NSNumber *)fromValue doubleValue]]; + } +#endif CGFloat fromFields[count]; [fromValue getValue:fromFields]; diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index a69491254..c0b1981e1 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -50,6 +50,7 @@ var AndroidTextInputAttributes = { multiline: true, password: true, placeholder: true, + placeholderTextColor: true, text: true, testID: true, underlineColorAndroid: true, @@ -514,6 +515,7 @@ var TextInput = React.createClass({ onLayout={this.props.onLayout} password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} + placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} underlineColorAndroid={this.props.underlineColorAndroid} children={children} diff --git a/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js b/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js new file mode 100644 index 000000000..4ee5a1f03 --- /dev/null +++ b/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js @@ -0,0 +1,55 @@ +/** + * 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. + * + * Utility class to provide the component owner hierarchy to native code for + * debugging purposes. + * + * @providesModule RCTDebugComponentOwnership + * @flow + */ + +'use strict'; + +var DebugComponentOwnershipModule = require('NativeModules').DebugComponentOwnershipModule; +var InspectorUtils = require('InspectorUtils'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); + +function componentToString(component) { + return component.getName ? component.getName() : 'Unknown'; +} + +function getRootTagForTag(tag: number): ?number { + var rootNodeID = ReactNativeTagHandles.tagToRootNodeID[tag]; + if (!rootNodeID) { + return null; + } + var rootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootNodeID); + if (!rootID) { + return null; + } + return ReactNativeTagHandles.rootNodeIDToTag[rootID]; +} + +module.exports = { + + /** + * Asynchronously returns the owner hierarchy as an array of strings. Request id is + * passed along to the native module so that the native module can identify the + * particular call instance. + * + * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text'] + */ + getOwnerHierarchy: function(requestID: number, tag: number) { + var rootTag = getRootTagForTag(tag); + var instance = InspectorUtils.findInstanceByNativeTag(rootTag, tag); + var ownerHierarchy = instance ? + InspectorUtils.getOwnerHierarchy(instance).map(componentToString) : + null; + DebugComponentOwnershipModule.receiveOwnershipHierarchy(requestID, tag, ownerHierarchy); + }, +}; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 8b6218882..b6337f670 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -23,6 +23,7 @@ /* globals GLOBAL: true, window: true */ // Just to make sure the JS gets packaged up. +require('RCTDebugComponentOwnership'); require('RCTDeviceEventEmitter'); require('PerformanceLogger'); diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index b4ae07c1a..e6785c5ad 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -285,7 +285,7 @@ RCT_EXPORT_MODULE() }]; id handler = [handlers lastObject]; if (!handler) { - RCTLogError(@"No suitable request handler found for %@", request); + RCTLogError(@"No suitable request handler found for %@", request.URL); } return handler; } @@ -322,6 +322,9 @@ RCT_EXPORT_MODULE() NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]]; if (request) { id handler = [self handlerForRequest:request]; + if (!handler) { + return; + } (void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) { if (data) { callback(nil, @{@"body": data, @"contentType": MIMEType}); diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 0ab8c0555..73f88d5ed 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -99,7 +99,14 @@ RCT_NOT_IMPLEMENTED(-init) error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview]; - RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); + + + NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) { + return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"]; + }]]; + RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews); + + vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; if (expectErrorBlock) { diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m new file mode 100644 index 000000000..838e8bc80 --- /dev/null +++ b/React/Base/RCTBatchedBridge.m @@ -0,0 +1,785 @@ +/** + * 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 + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTContextExecutor.h" +#import "RCTFrameUpdate.h" +#import "RCTJavaScriptLoader.h" +#import "RCTLog.h" +#import "RCTModuleData.h" +#import "RCTModuleMethod.h" +#import "RCTPerformanceLogger.h" +#import "RCTPerfStats.h" +#import "RCTProfile.h" +#import "RCTRedBox.h" +#import "RCTSourceCode.h" +#import "RCTSparseArray.h" +#import "RCTUtils.h" + +#define RCTAssertJSThread() \ + RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTContextExecutor"] || \ + [[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \ + @"This method must be called on JS thread") + +NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; +NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; + +/** + * Must be kept in sync with `MessageQueue.js`. + */ +typedef NS_ENUM(NSUInteger, RCTBridgeFields) { + RCTBridgeFieldRequestModuleIDs = 0, + RCTBridgeFieldMethodIDs, + RCTBridgeFieldParamss, +}; + +RCT_EXTERN NSArray *RCTGetModuleClasses(void); + +static id RCTLatestExecutor = nil; +id RCTGetLatestExecutor(void); +id RCTGetLatestExecutor(void) +{ + return RCTLatestExecutor; +} + +@interface RCTBatchedBridge : RCTBridge + +@property (nonatomic, weak) RCTBridge *parentBridge; + +@end + +@implementation RCTBatchedBridge +{ + BOOL _loading; + __weak id _javaScriptExecutor; + NSMutableArray *_modules; + NSDictionary *_modulesByName; + CADisplayLink *_mainDisplayLink; + CADisplayLink *_jsDisplayLink; + NSMutableSet *_frameUpdateObservers; + NSMutableArray *_scheduledCalls; + RCTSparseArray *_scheduledCallbacks; +} + +@synthesize valid = _valid; + +- (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; + _modules = [[NSMutableArray alloc] init]; + _frameUpdateObservers = [[NSMutableSet alloc] init]; + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + + if (RCT_DEV) { + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + + /** + * Initialize and register bridge modules *before* adding the display link + * so we don't have threading issues + */ + [self registerModules]; + + /** + * Start the application script + */ + [self initJS]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL + moduleProvider:(__unused RCTBridgeModuleProviderBlock)block + launchOptions:(__unused NSDictionary *)launchOptions) + +- (void)setUp {} +- (void)bindKeys {} + +- (void)reload +{ + [_parentBridge reload]; +} + +- (Class)executorClass +{ + return _parentBridge.executorClass ?: [RCTContextExecutor class]; +} + +- (void)setExecutorClass:(Class)executorClass +{ + RCTAssertMainThread(); + + _parentBridge.executorClass = executorClass; +} + +- (BOOL)isLoading +{ + return _loading; +} + +- (BOOL)isValid +{ + return _valid; +} + +- (void)registerModules +{ + RCTAssertMainThread(); + + // Register passed-in module instances + NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; + for (id module in self.moduleProvider ? self.moduleProvider() : nil) { + preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; + } + + // Instantiate modules + _modules = [[NSMutableArray alloc] init]; + NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy]; + for (Class moduleClass in RCTGetModuleClasses()) { + NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); + + // Check if module instance has already been registered for this name + id module = modulesByName[moduleName]; + + if (module) { + // Preregistered instances takes precedence, no questions asked + if (!preregisteredModules[moduleName]) { + // It's OK to have a name collision as long as the second instance is nil + RCTAssert([[moduleClass alloc] init] == nil, + @"Attempted to register RCTBridgeModule class %@ for the name " + "'%@', but name was already registered by class %@", moduleClass, + moduleName, [modulesByName[moduleName] class]); + } + if ([module class] != moduleClass) { + RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered " + "in the project, but name was already registered by class %@." + "That's fine if it's intentional - just letting you know.", + moduleClass, moduleName, [modulesByName[moduleName] class]); + } + } else { + // Module name hasn't been used before, so go ahead and instantiate + module = [[moduleClass alloc] init]; + } + if (module) { + modulesByName[moduleName] = module; + } + } + + // Store modules + _modulesByName = [modulesByName copy]; + + /** + * 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 = _modulesByName[RCTBridgeModuleNameForClass(self.executorClass)]; + RCTLatestExecutor = _javaScriptExecutor; + RCTSetExecutorID(_javaScriptExecutor); + + [_javaScriptExecutor setUp]; + + // Set bridge + for (id module in _modulesByName.allValues) { + if ([module respondsToSelector:@selector(setBridge:)]) { + module.bridge = self; + } + + RCTModuleData *moduleData = [[RCTModuleData alloc] initWithExecutor:_javaScriptExecutor + uid:@(_modules.count) + instance:module]; + [_modules addObject:moduleData]; + + if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { + [_frameUpdateObservers addObject:moduleData]; + } + } +} + +- (void)initJS +{ + RCTAssertMainThread(); + + // Inject module data into JS context + NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; + for (RCTModuleData *moduleData in _modules) { + config[moduleData.name] = moduleData.config; + } + NSString *configJSON = RCTJSONStringify(@{ + @"remoteModuleConfig": config, + }, NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [_javaScriptExecutor injectJSONText:configJSON + asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback: + ^(__unused id err) { + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); + + NSURL *bundleURL = _parentBridge.bundleURL; + if (_javaScriptExecutor == nil) { + + /** + * HACK (tadeu): If it failed to connect to the debugger, set loading to NO + * so we can attempt to reload again. + */ + _loading = NO; + + } else if (!bundleURL) { + + // Allow testing without a script + dispatch_async(dispatch_get_main_queue(), ^{ + _loading = NO; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + }); + } else { + + RCTProfileBeginEvent(); + RCTPerformanceLoggerStart(RCTPLScriptDownload); + RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; + [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { + RCTPerformanceLoggerEnd(RCTPLScriptDownload); + RCTProfileEndEvent(@"JavaScript dowload", @"init,download", @[]); + + _loading = NO; + if (!self.isValid) { + return; + } + + [[RCTRedBox sharedInstance] dismiss]; + + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = bundleURL; + sourceCodeModule.scriptText = script; + if (error) { + + NSArray *stack = [error userInfo][@"stack"]; + if (stack) { + [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] + withStack:stack]; + } else { + [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] + withDetails:[error localizedFailureReason]]; + } + + NSDictionary *userInfo = @{@"bridge": self, @"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:_parentBridge + userInfo:userInfo]; + + } else { + + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { + + if (!loadError) { + + /** + * Register the display link to start sending js calls after everything + * is setup + */ + NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; + [_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + } else { + [[RCTRedBox sharedInstance] showErrorMessage:[loadError localizedDescription] + withDetails:[loadError localizedFailureReason]]; + } + }]; + } + }]; + } +} + +- (NSDictionary *)modules +{ + RCTAssert(!self.isValid || _modulesByName != nil, @"Bridge modules have not yet been initialized. " + "You may be trying to access a module too early in the startup procedure."); + + return _modulesByName; +} + +#pragma mark - RCTInvalidating + +- (void)invalidate +{ + if (!self.isValid) { + return; + } + + RCTAssertMainThread(); + + _valid = NO; + if (RCTLatestExecutor == _javaScriptExecutor) { + RCTLatestExecutor = nil; + } + + void (^mainThreadInvalidate)(void) = ^{ + RCTAssertMainThread(); + + [_mainDisplayLink invalidate]; + _mainDisplayLink = nil; + + // Invalidate modules + dispatch_group_t group = dispatch_group_create(); + for (RCTModuleData *moduleData in _modules) { + if ([moduleData.instance respondsToSelector:@selector(invalidate)]) { + [moduleData dispatchBlock:^{ + [(id)moduleData.instance invalidate]; + } dispatchGroup:group]; + } + moduleData.queue = nil; + } + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + _modules = nil; + _modulesByName = nil; + _frameUpdateObservers = nil; + }); + }; + + if (!_javaScriptExecutor) { + + // No JS thread running + mainThreadInvalidate(); + return; + } + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + _javaScriptExecutor = nil; + + [_jsDisplayLink invalidate]; + _jsDisplayLink = nil; + + /** + * Main Thread deallocations + */ + dispatch_async(dispatch_get_main_queue(), mainThreadInvalidate); + + }]; +} + +#pragma mark - RCTBridge methods + +/** + * Public. Can be invoked from any thread. + */ +- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args +{ + NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; + + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[ids[0], ids[1], args ?: @[]] + context:RCTGetExecutorID(_javaScriptExecutor)]; +} + +/** + * Private hack to support `setTimeout(fn, 0)` + */ +- (void)_immediatelyCallTimer:(NSNumber *)timer +{ + RCTAssertJSThread(); + + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[@"JSTimersExecution", @"callTimers", @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; + + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + } +} + +- (void)enqueueApplicationScript:(NSString *)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; + } + + RCTProfileBeginEvent(); + NSNumber *context = RCTGetExecutorID(_javaScriptExecutor); + [_javaScriptExecutor executeJSCall:@"BatchedBridge" + method:@"flushedQueue" + arguments:@[] + context:context + callback:^(id json, NSError *error) { + RCTProfileEndEvent(@"FetchApplicationScriptCallbacks", @"js_call,init", @{ + @"json": RCTNullIfNil(json), + @"error": RCTNullIfNil(error), + }); + + [self _handleBuffer:json context:context]; + + onComplete(error); + }]; + }]; +} + +#pragma mark - Payload Generation + +/** + * TODO: Completely remove `context` - no longer needed + */ +- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +{ + [self _invokeAndProcessModule:module + method:method + arguments:args + context:RCTGetExecutorID(_javaScriptExecutor)]; +} + +/** + * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer, + * on the JS thread, but only in non-batched mode. + */ +- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context +{ + /** + * AnyThread + */ + + RCTProfileBeginFlowEvent(); + + __weak RCTBatchedBridge *weakSelf = self; + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileEndFlowEvent(); + RCTProfileBeginEvent(); + + RCTBatchedBridge *strongSelf = weakSelf; + if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { + return; + } + + + RCT_IF_DEV(NSNumber *callID = _RCTProfileBeginFlowEvent();) + id call = @{ + @"js_args": @{ + @"module": module, + @"method": method, + @"args": args, + }, + @"context": context ?: @0, + RCT_IF_DEV(@"call_id": callID,) + }; + if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { + strongSelf->_scheduledCallbacks[args[0]] = call; + } else { + [strongSelf->_scheduledCalls addObject:call]; + } + + RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + }]; +} + +- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context +{ + RCTAssertJSThread(); + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; + + RCTJavaScriptCallback processResponse = ^(id json, __unused NSError *error) { + if (!self.isValid) { + return; + } + [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; + [self _handleBuffer:json context:context]; + }; + + [_javaScriptExecutor executeJSCall:module + method:method + arguments:args + context:context + callback:processResponse]; +} + +#pragma mark - Payload Processing + +- (void)_handleBuffer:(id)buffer context:(NSNumber *)context +{ + RCTAssertJSThread(); + + if (buffer == nil || buffer == (id)kCFNull) { + return; + } + + NSArray *requestsArray = [RCTConvert NSArray:buffer]; + +#if RCT_DEBUG + + if (![buffer isKindOfClass:[NSArray class]]) { + RCTLogError(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class])); + return; + } + + for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) { + id field = [requestsArray objectAtIndex:fieldIndex]; + if (![field isKindOfClass:[NSArray class]]) { + RCTLogError(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class])); + return; + } + } + +#endif + + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; + NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; + NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; + + NSUInteger numRequests = [moduleIDs count]; + + if (RCT_DEBUG && (numRequests != methodIDs.count || numRequests != paramsArrays.count)) { + RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); + return; + } + + NSMapTable *buckets = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:_modules.count]; + for (NSUInteger i = 0; i < numRequests; i++) { + RCTModuleData *moduleData = _modules[[moduleIDs[i] integerValue]]; + NSMutableOrderedSet *set = [buckets objectForKey:moduleData]; + if (!set) { + set = [[NSMutableOrderedSet alloc] init]; + [buckets setObject:set forKey:moduleData]; + } + [set addObject:@(i)]; + } + + for (RCTModuleData *moduleData in buckets) { + RCTProfileBeginFlowEvent(); + + [moduleData dispatchBlock:^{ + RCTProfileEndFlowEvent(); + RCTProfileBeginEvent(); + + NSOrderedSet *calls = [buckets objectForKey:moduleData]; + @autoreleasepool { + for (NSNumber *indexObj in calls) { + NSUInteger index = indexObj.unsignedIntegerValue; + [self _handleRequestNumber:index + moduleID:[moduleIDs[index] integerValue] + methodID:[methodIDs[index] integerValue] + params:paramsArrays[index] + context:context]; + } + } + RCTProfileEndEvent(RCTCurrentThreadName(), @"objc_call,dispatch_async", @{ @"calls": @(calls.count) }); + }]; + } + + // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? + for (RCTModuleData *moduleData in _modules) { + if ([moduleData.instance respondsToSelector:@selector(batchDidComplete)]) { + [moduleData dispatchBlock:^{ + [moduleData.instance batchDidComplete]; + }]; + } + } +} + +- (BOOL)_handleRequestNumber:(NSUInteger)i + moduleID:(NSUInteger)moduleID + methodID:(NSUInteger)methodID + params:(NSArray *)params + context:(NSNumber *)context +{ + if (!self.isValid) { + return NO; + } + + if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) { + RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); + return NO; + } + + + RCTProfileBeginEvent(); + + RCTModuleData *moduleData = _modules[moduleID]; + if (RCT_DEBUG && !moduleData) { + RCTLogError(@"No module found for id '%zd'", moduleID); + return NO; + } + + RCTModuleMethod *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 context:context]; + } + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, moduleData.name, params, exception); + if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw exception; + } + } + + RCTProfileEndEvent(@"Invoke callback", @"objc_call", @{ + @"module": method.moduleClassName, + @"method": method.JSMethodName, + @"selector": NSStringFromSelector(method.selector), + @"args": RCTJSONStringify(RCTNullIfNil(params), NULL), + }); + + return YES; +} + +- (void)_jsThreadUpdate:(CADisplayLink *)displayLink +{ + RCTAssertJSThread(); + RCTProfileBeginEvent(); + + RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; + for (RCTModuleData *moduleData in _frameUpdateObservers) { + id observer = (id)moduleData.instance; + if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { + RCT_IF_DEV(NSString *name = [NSString stringWithFormat:@"[%@ didUpdateFrame:%f]", observer, displayLink.timestamp];) + RCTProfileBeginFlowEvent(); + + [moduleData dispatchBlock:^{ + RCTProfileEndFlowEvent(); + RCTProfileBeginEvent(); + [observer didUpdateFrame:frameUpdate]; + RCTProfileEndEvent(name, @"objc_call,fps", nil); + }]; + } + } + + NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; + NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); + calls = [calls filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock: + ^BOOL(NSDictionary *call, __unused NSDictionary *bindings) { + return [call[@"context"] isEqualToNumber:currentExecutorID]; + }]]; + + RCT_IF_DEV( + RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + + for (NSDictionary *call in calls) { + _RCTProfileEndFlowEvent(call[@"call_id"]); + } + ) + + if (calls.count > 0) { + _scheduledCalls = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"processBatch" + arguments:@[[calls valueForKey:@"js_args"]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + } + + RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.perfStats.jsGraph tick:displayLink.timestamp]; + }); +} + +- (void)_mainThreadUpdate:(CADisplayLink *)displayLink +{ + RCTAssertMainThread(); + + RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); + + [self.perfStats.uiGraph tick:displayLink.timestamp]; +} + +- (void)startProfiling +{ + RCTAssertMainThread(); + + if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { + RCTLogError(@"To run the profiler you must be running from the dev server"); + return; + } + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileInit(self); + }]; +} + +- (void)stopProfiling +{ + RCTAssertMainThread(); + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + NSString *log = RCTProfileEnd(self); + NSURL *bundleURL = _parentBridge.bundleURL; + NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; + NSURL *URL = [NSURL URLWithString:URLString]; + NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; + URLRequest.HTTPMethod = @"POST"; + [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + NSURLSessionTask *task = + [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest + fromData:[log dataUsingEncoding:NSUTF8StringEncoding] + completionHandler: + ^(__unused NSData *data, __unused NSURLResponse *response, NSError *error) { + if (error) { + RCTLogError(@"%@", error.localizedDescription); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [[[UIAlertView alloc] initWithTitle:@"Profile" + message:@"The profile has been generated, check the dev server log for instructions." + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }); + } + }]; + + [task resume]; + }]; +} + +@end diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 35183437d..1d70a6367 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -118,6 +118,11 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); */ @property (nonatomic, readonly, getter=isLoading) BOOL loading; +/** + * The block passed in the constructor with pre-initialized modules + */ +@property (nonatomic, copy, readonly) RCTBridgeModuleProviderBlock moduleProvider; + /** * Reload the bundle and reset executor & modules. Safe to call from any thread. */ diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index e844cd8b1..28fcc460f 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -9,61 +9,48 @@ #import "RCTBridge.h" -#import -#import #import -#import "RCTContextExecutor.h" -#import "RCTConvert.h" #import "RCTEventDispatcher.h" -#import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" -#import "RCTPerfStats.h" #import "RCTPerformanceLogger.h" -#import "RCTProfile.h" -#import "RCTRedBox.h" -#import "RCTRootView.h" -#import "RCTSourceCode.h" -#import "RCTSparseArray.h" #import "RCTUtils.h" NSString *const RCTReloadNotification = @"RCTReloadNotification"; NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; -/** - * Must be kept in sync with `MessageQueue.js`. - */ -typedef NS_ENUM(NSUInteger, RCTBridgeFields) { - RCTBridgeFieldRequestModuleIDs = 0, - RCTBridgeFieldMethodIDs, - RCTBridgeFieldParamss, -}; +@class RCTBatchedBridge; -typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { - RCTJavaScriptFunctionKindNormal, - RCTJavaScriptFunctionKindAsync, -}; +@interface RCTBatchedBridge : RCTBridge -#define RCTAssertJSThread() \ - RCTAssert(![NSStringFromClass([_javaScriptExecutor class]) isEqualToString:@"RCTContextExecutor"] || \ - [[[NSThread currentThread] name] isEqualToString:@"com.facebook.React.JavaScript"], \ - @"This method must be called on JS thread") +@property (nonatomic, weak) RCTBridge *parentBridge; -NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; -NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; +- (instancetype)initWithParentBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; + +@end + +@interface RCTBridge () + +@property (nonatomic, strong) RCTBatchedBridge *batchedBridge; + +@end + +RCT_EXTERN id RCTGetLatestExecutor(void); + +static NSMutableArray *RCTModuleClasses; +NSArray *RCTGetModuleClasses(void); +NSArray *RCTGetModuleClasses(void) +{ + return RCTModuleClasses; +} -static NSDictionary *RCTModuleIDsByName; -static NSArray *RCTModuleNamesByID; -static NSArray *RCTModuleClassesByID; void RCTRegisterModule(Class moduleClass) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - RCTModuleIDsByName = [[NSMutableDictionary alloc] init]; - RCTModuleNamesByID = [[NSMutableArray alloc] init]; - RCTModuleClassesByID = [[NSMutableArray alloc] init]; + RCTModuleClasses = [[NSMutableArray alloc] init]; }); RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], @@ -71,11 +58,7 @@ void RCTRegisterModule(Class moduleClass) NSStringFromClass(moduleClass)); // Register module - NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); - ((NSMutableDictionary *)RCTModuleIDsByName)[moduleName] = @(RCTModuleNamesByID.count); - [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; - [(NSMutableArray *)RCTModuleClassesByID addObject:moduleClass]; - + [RCTModuleClasses addObject:moduleClass]; } /** @@ -84,7 +67,7 @@ void RCTRegisterModule(Class moduleClass) NSString *RCTBridgeModuleNameForClass(Class cls) { NSString *name = nil; - if ([cls respondsToSelector:@selector(moduleName)]) { + if ([cls respondsToSelector:NSSelectorFromString(@"moduleName")]) { name = [cls valueForKey:@"moduleName"]; } if ([name length] == 0) { @@ -96,475 +79,9 @@ NSString *RCTBridgeModuleNameForClass(Class cls) return name; } -// TODO: Can we just replace RCTMakeError with this function instead? -static NSDictionary *RCTJSErrorFromNSError(NSError *error) -{ - NSString *errorMessage; - NSArray *stackTrace = [NSThread callStackSymbols]; - NSMutableDictionary *errorInfo = - [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; - - if (error) { - errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; - errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; - errorInfo[@"code"] = @(error.code); - } else { - errorMessage = @"Unknown error from a native module"; - errorInfo[@"domain"] = RCTErrorDomain; - errorInfo[@"code"] = @-1; - } - - return RCTMakeError(errorMessage, nil, errorInfo); -} - -@class RCTBatchedBridge; - -@interface RCTBridge () - -@property (nonatomic, strong) RCTBatchedBridge *batchedBridge; -@property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; - -- (void)_invokeAndProcessModule:(NSString *)module - method:(NSString *)method - arguments:(NSArray *)args - context:(NSNumber *)context; - -@end - -@interface RCTBatchedBridge : RCTBridge - -@property (nonatomic, weak) RCTBridge *parentBridge; - -- (instancetype)initWithParentBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; - -- (void)_actuallyInvokeAndProcessModule:(NSString *)module - method:(NSString *)method - arguments:(NSArray *)args - context:(NSNumber *)context; - -@end - -/** - * This private class is used as a container for exported method info - */ -@interface RCTModuleMethod : NSObject - -@property (nonatomic, copy, readonly) NSString *moduleClassName; -@property (nonatomic, copy, readonly) NSString *JSMethodName; -@property (nonatomic, assign, readonly) SEL selector; -@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; - -@end - -@implementation RCTModuleMethod -{ - Class _moduleClass; - SEL _selector; - NSMethodSignature *_methodSignature; - NSArray *_argumentBlocks; -} - -- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName - JSMethodName:(NSString *)JSMethodName - moduleClass:(Class)moduleClass -{ - if ((self = [super init])) { - static NSRegularExpression *typeRegex; - static NSRegularExpression *selectorRegex; - if (!typeRegex) { - NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))"; - NSString *constPattern = @"(?:const)"; - NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern]; - NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern]; - typeRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; - - selectorRegex = [[NSRegularExpression alloc] initWithPattern:@"(?<=:).*?(?=[a-zA-Z_]+:|$)" options:0 error:NULL]; - } - - NSMutableArray *argumentNames = [NSMutableArray array]; - [typeRegex enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) { - NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]]; - [argumentNames addObject:argumentName]; - }]; - - // Remove the parameters' type and name - objCMethodName = [selectorRegex stringByReplacingMatchesInString:objCMethodName - options:0 - range:NSMakeRange(0, objCMethodName.length) - withTemplate:@""]; - // Remove any spaces since `selector : (Type)name` is a valid syntax - objCMethodName = [objCMethodName stringByReplacingOccurrencesOfString:@" " withString:@""]; - - _moduleClass = moduleClass; - _moduleClassName = NSStringFromClass(_moduleClass); - _selector = NSSelectorFromString(objCMethodName); - _JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({ - NSString *methodName = NSStringFromSelector(_selector); - NSRange colonRange = [methodName rangeOfString:@":"]; - if (colonRange.length) { - methodName = [methodName substringToIndex:colonRange.location]; - } - methodName; - }); - - - // Get method signature - _methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; - - // Process arguments - NSUInteger numberOfArguments = _methodSignature.numberOfArguments; - NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; - -#define RCT_ARG_BLOCK(_logic) \ - [argumentBlocks addObject:^(__unused RCTBridge *bridge, __unused NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { \ - _logic \ - [invocation setArgument:&value atIndex:index]; \ - }]; \ - - void (^addBlockArgument)(void) = ^{ - RCT_ARG_BLOCK( - - if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSArray *args) { - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, args] - context:context]; - } : ^(__unused NSArray *unused) {}); - ) - }; - - void (^defaultCase)(const char *) = ^(const char *argumentType) { - static const char *blockType = @encode(typeof(^{})); - if (!strcmp(argumentType, blockType)) { - addBlockArgument(); - } else { - RCT_ARG_BLOCK( id value = json; ) - } - }; - - for (NSUInteger i = 2; i < numberOfArguments; i++) { - const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; - - NSString *argumentName = argumentNames[i - 2]; - SEL selector = NSSelectorFromString([argumentName stringByAppendingString:@":"]); - if ([RCTConvert respondsToSelector:selector]) { - switch (argumentType[0]) { - -#define RCT_CONVERT_CASE(_value, _type) \ -case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ - RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ - break; \ -} - - RCT_CONVERT_CASE(':', SEL) - RCT_CONVERT_CASE('*', const char *) - RCT_CONVERT_CASE('c', char) - RCT_CONVERT_CASE('C', unsigned char) - RCT_CONVERT_CASE('s', short) - RCT_CONVERT_CASE('S', unsigned short) - RCT_CONVERT_CASE('i', int) - RCT_CONVERT_CASE('I', unsigned int) - RCT_CONVERT_CASE('l', long) - RCT_CONVERT_CASE('L', unsigned long) - RCT_CONVERT_CASE('q', long long) - RCT_CONVERT_CASE('Q', unsigned long long) - RCT_CONVERT_CASE('f', float) - RCT_CONVERT_CASE('d', double) - RCT_CONVERT_CASE('B', BOOL) - RCT_CONVERT_CASE('@', id) - RCT_CONVERT_CASE('^', void *) - - case '{': { - [argumentBlocks addObject:^(__unused RCTBridge *bridge, __unused NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { - NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; - void *returnValue = malloc(methodSignature.methodReturnLength); - NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - [_invocation setTarget:[RCTConvert class]]; - [_invocation setSelector:selector]; - [_invocation setArgument:&json atIndex:2]; - [_invocation invoke]; - [_invocation getReturnValue:returnValue]; - - [invocation setArgument:returnValue atIndex:index]; - - free(returnValue); - }]; - break; - } - - default: - defaultCase(argumentType); - } - } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { - addBlockArgument(); - } else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) { - RCTAssert(i == numberOfArguments - 2, - @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseResolveBlock value = (^(id result) { - NSArray *arguments = result ? @[result] : @[]; - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, arguments] - context:context]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) { - RCTAssert(i == numberOfArguments - 1, - @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { - NSDictionary *errorJSON = RCTJSErrorFromNSError(error); - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[errorJSON]] - context:context]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else { - - // Unknown argument type - RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" - " to support this type.", argumentName, [self methodName]); - } - } - - _argumentBlocks = [argumentBlocks copy]; - } - - return self; -} - -- (void)invokeWithBridge:(RCTBridge *)bridge - module:(id)module - arguments:(NSArray *)arguments - context:(NSNumber *)context -{ - if (RCT_DEBUG) { - - // Sanity check - RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ - %@ on a module of class %@", [self methodName], [module class]); - - // Safety check - if (arguments.count != _argumentBlocks.count) { - NSInteger actualCount = arguments.count; - NSInteger expectedCount = _argumentBlocks.count; - - // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions - if (_functionKind == RCTJavaScriptFunctionKindAsync) { - actualCount -= 2; - expectedCount -= 2; - } - - RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", - RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, - actualCount, expectedCount); - return; - } - } - - // Create invocation (we can't re-use this as it wouldn't be thread-safe) - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:_methodSignature]; - [invocation setArgument:&_selector atIndex:1]; - [invocation retainArguments]; - - // Set arguments - NSUInteger index = 0; - for (id json in arguments) { - id arg = RCTNilIfNull(json); - void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; - block(bridge, context, invocation, index + 2, arg); - index++; - } - - // Invoke method - [invocation invokeWithTarget:module]; -} - -- (NSString *)methodName -{ - return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass, - NSStringFromSelector(_selector)]; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@();>", - [self class], self, [self methodName], _JSMethodName]; -} - -@end - -/** - * This function parses the exported methods inside RCTBridgeModules and - * generates an array of arrays of RCTModuleMethod objects, keyed - * by module index. - */ -static RCTSparseArray *RCTExportedMethodsByModuleID(void) -{ - static RCTSparseArray *methodsByModuleID; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - methodsByModuleID = [[RCTSparseArray alloc] initWithCapacity:[RCTModuleClassesByID count]]; - - [RCTModuleClassesByID enumerateObjectsUsingBlock: - ^(Class moduleClass, NSUInteger moduleID, __unused BOOL *stop) { - - methodsByModuleID[moduleID] = [[NSMutableArray alloc] init]; - - unsigned int methodCount; - Method *methods = class_copyMethodList(object_getClass(moduleClass), &methodCount); - - for (unsigned int i = 0; i < methodCount; i++) { - Method method = methods[i]; - SEL selector = method_getName(method); - if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { - IMP imp = method_getImplementation(method); - NSArray *entries = ((NSArray *(*)(id, SEL))imp)(moduleClass, selector); - RCTModuleMethod *moduleMethod = - [[RCTModuleMethod alloc] initWithObjCMethodName:entries[1] - JSMethodName:entries[0] - moduleClass:moduleClass]; - - [methodsByModuleID[moduleID] addObject:moduleMethod]; - } - } - - free(methods); - - }]; - - }); - - return methodsByModuleID; -} - -/** - * 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": "remoteAsync" - * }, - * etc... - * }, - * "constants": { - * ... - * } - * }, - * etc... - */ -static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) -{ - static NSMutableDictionary *remoteModuleConfigByClassName; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - remoteModuleConfigByClassName = [[NSMutableDictionary alloc] init]; - [RCTModuleClassesByID enumerateObjectsUsingBlock: - ^(Class moduleClass, NSUInteger moduleID, __unused BOOL *stop) { - - NSArray *methods = RCTExportedMethodsByModuleID()[moduleID]; - NSMutableDictionary *methodsByName = [NSMutableDictionary dictionaryWithCapacity:methods.count]; - [methods enumerateObjectsUsingBlock: - ^(RCTModuleMethod *method, NSUInteger methodID, __unused BOOL *_stop) { - methodsByName[method.JSMethodName] = @{ - @"methodID": @(methodID), - @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", - }; - }]; - - NSDictionary *module = @{ - @"moduleID": @(moduleID), - @"methods": methodsByName - }; - - remoteModuleConfigByClassName[NSStringFromClass(moduleClass)] = module; - }]; - }); - - // Create config - NSMutableDictionary *moduleConfig = [[NSMutableDictionary alloc] init]; - [modulesByName enumerateKeysAndObjectsUsingBlock: - ^(NSString *moduleName, id module, __unused BOOL *stop) { - - // Add constants - NSMutableDictionary *config = remoteModuleConfigByClassName[NSStringFromClass([module class])]; - if ([module respondsToSelector:@selector(constantsToExport)]) { - NSDictionary *constants = [module constantsToExport]; - if (constants) { - NSMutableDictionary *mutableConfig = [NSMutableDictionary dictionaryWithDictionary:config]; - mutableConfig[@"constants"] = constants; // There's no real need to copy this - config = mutableConfig; // Nor this - receiver is unlikely to mutate it - } - } - - moduleConfig[moduleName] = config; - }]; - - return moduleConfig; -} - -@interface RCTFrameUpdate (Private) - -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; - -@end - -@implementation RCTFrameUpdate - -- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink -{ - if ((self = [super init])) { - _timestamp = displayLink.timestamp; - _deltaTime = displayLink.duration; - } - return self; -} - -@end @implementation RCTBridge -static id _latestJSExecutor; dispatch_queue_t RCTJSThread; + (void)initialize @@ -589,7 +106,7 @@ dispatch_queue_t RCTJSThread; { if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) { - if (![RCTModuleClassesByID containsObject:cls]) { + if (![RCTModuleClasses containsObject:cls]) { RCTLogError(@"Class %@ was not exported. Did you forget to use " "RCT_EXPORT_MODULE()?", NSStringFromClass(cls)); } @@ -705,15 +222,15 @@ RCT_NOT_IMPLEMENTED(-init) + (void)logMessage:(NSString *)message level:(NSString *)level { dispatch_async(dispatch_get_main_queue(), ^{ - if (!_latestJSExecutor.isValid) { + if (!RCTGetLatestExecutor().isValid) { return; } - [_latestJSExecutor executeJSCall:@"RCTLog" - method:@"logIfNoNativeHook" - arguments:@[level, message] - context:RCTGetExecutorID(_latestJSExecutor) - callback:^(__unused id json, __unused NSError *error) {}]; + [RCTGetLatestExecutor() executeJSCall:@"RCTLog" + method:@"logIfNoNativeHook" + arguments:@[level, message] + context:RCTGetExecutorID(RCTGetLatestExecutor()) + callback:^(__unused id json, __unused NSError *error) {}]; }); } @@ -740,808 +257,3 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(__unused NSString *)module context:(__unused NSNumber *)context) @end - -@implementation RCTBatchedBridge -{ - BOOL _loading; - __weak id _javaScriptExecutor; - RCTSparseArray *_modulesByID; - RCTSparseArray *_queuesByID; - NSDictionary *_modulesByName; - CADisplayLink *_mainDisplayLink; - CADisplayLink *_jsDisplayLink; - NSMutableSet *_frameUpdateObservers; - NSMutableArray *_scheduledCalls; - RCTSparseArray *_scheduledCallbacks; -} - -@synthesize valid = _valid; - -- (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; - _frameUpdateObservers = [[NSMutableSet alloc] init]; - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; - _queuesByID = [[RCTSparseArray alloc] init]; - _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; - - if (RCT_DEV) { - _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; - [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - } - - /** - * Initialize and register bridge modules *before* adding the display link - * so we don't have threading issues - */ - [self registerModules]; - - /** - * Start the application script - */ - [self initJS]; - } - return self; -} - -- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL - moduleProvider:(__unused RCTBridgeModuleProviderBlock)block - launchOptions:(__unused NSDictionary *)launchOptions -{ - return [self initWithParentBridge:nil]; -} - -/** - * Override to ensure that we won't create another nested bridge - */ -- (void)setUp {} - -- (void)reload -{ - [_parentBridge reload]; -} - -- (Class)executorClass -{ - return _parentBridge.executorClass ?: [RCTContextExecutor class]; -} - -- (void)setExecutorClass:(Class)executorClass -{ - RCTAssertMainThread(); - - _parentBridge.executorClass = executorClass; -} - -- (BOOL)isLoading -{ - return _loading; -} - -- (BOOL)isValid -{ - return _valid; -} - -- (void)registerModules -{ - RCTAssertMainThread(); - - // Register passed-in module instances - NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; - for (id module in self.moduleProvider ? self.moduleProvider() : nil) { - preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; - } - - // Instantiate modules - _modulesByID = [[RCTSparseArray alloc] init]; - NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy]; - [RCTModuleClassesByID enumerateObjectsUsingBlock: - ^(Class moduleClass, NSUInteger moduleID, __unused BOOL *stop) { - NSString *moduleName = RCTModuleNamesByID[moduleID]; - // Check if module instance has already been registered for this name - id module = modulesByName[moduleName]; - if (module) { - // Preregistered instances takes precedence, no questions asked - if (!preregisteredModules[moduleName]) { - // It's OK to have a name collision as long as the second instance is nil - RCTAssert([[moduleClass alloc] init] == nil, - @"Attempted to register RCTBridgeModule class %@ for the name " - "'%@', but name was already registered by class %@", moduleClass, - moduleName, [modulesByName[moduleName] class]); - } - if ([module class] != moduleClass) { - RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered " - "in the project, but name was already registered by class %@." - "That's fine if it's intentional - just letting you know.", - moduleClass, moduleName, [modulesByName[moduleName] class]); - } - } else { - // Module name hasn't been used before, so go ahead and instantiate - module = [[moduleClass alloc] init]; - } - if (module) { - // Store module instance - _modulesByID[moduleID] = modulesByName[moduleName] = module; - } - }]; - - // Store modules - _modulesByName = [modulesByName copy]; - - /** - * 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 = _modulesByName[RCTBridgeModuleNameForClass(self.executorClass)]; - _latestJSExecutor = _javaScriptExecutor; - RCTSetExecutorID(_javaScriptExecutor); - - [_javaScriptExecutor setUp]; - - // Set bridge - for (id module in _modulesByName.allValues) { - if ([module respondsToSelector:@selector(setBridge:)]) { - module.bridge = self; - } - } - - // Set/get method queues - [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, __unused BOOL *stop) { - - dispatch_queue_t queue = nil; - BOOL implementsMethodQueue = [module respondsToSelector:@selector(methodQueue)]; - if (implementsMethodQueue) { - queue = [module methodQueue]; - } - if (!queue) { - - // Need to cache queueNames because they aren't retained by dispatch_queue - static NSMutableDictionary *queueNames; - if (!queueNames) { - queueNames = [[NSMutableDictionary alloc] init]; - } - NSString *moduleName = RCTBridgeModuleNameForClass([module class]); - NSString *queueName = queueNames[moduleName]; - if (!queueName) { - queueName = [NSString stringWithFormat:@"com.facebook.React.%@Queue", moduleName]; - queueNames[moduleName] = queueName; - } - - // Create new queue - queue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL); - - // assign it to the module - if (implementsMethodQueue) { - @try { - [(id)module setValue:queue forKey:@"methodQueue"]; - } - @catch (NSException *exception) { - RCTLogError(@"%@ is returning nil for it's methodQueue, which is not " - "permitted. You must either return a pre-initialized " - "queue, or @synthesize the methodQueue to let the bridge " - "create a queue for you.", moduleName); - } - } - } - _queuesByID[moduleID] = queue; - - if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { - [_frameUpdateObservers addObject:module]; - } - }]; -} - -- (void)initJS -{ - RCTAssertMainThread(); - - // Inject module data into JS context - NSString *configJSON = RCTJSONStringify(@{ - @"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName), - }, NULL); - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - [_javaScriptExecutor injectJSONText:configJSON - asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback: - ^(__unused id err) { - dispatch_semaphore_signal(semaphore); - }]; - - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW); - - NSURL *bundleURL = _parentBridge.bundleURL; - if (_javaScriptExecutor == nil) { - - /** - * HACK (tadeu): If it failed to connect to the debugger, set loading to NO - * so we can attempt to reload again. - */ - _loading = NO; - - } else if (!bundleURL) { - - // Allow testing without a script - dispatch_async(dispatch_get_main_queue(), ^{ - _loading = NO; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:_parentBridge - userInfo:@{ @"bridge": self }]; - }); - } else { - - RCTProfileBeginEvent(); - RCTPerformanceLoggerStart(RCTPLScriptDownload); - RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; - [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { - RCTPerformanceLoggerEnd(RCTPLScriptDownload); - RCTProfileEndEvent(@"JavaScript dowload", @"init,download", @[]); - - _loading = NO; - if (!self.isValid) { - return; - } - - [[RCTRedBox sharedInstance] dismiss]; - - RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = bundleURL; - sourceCodeModule.scriptText = script; - if (error) { - - NSArray *stack = [error userInfo][@"stack"]; - if (stack) { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] - withStack:stack]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] - withDetails:[error localizedFailureReason]]; - } - - NSDictionary *userInfo = @{@"bridge": self, @"error": error}; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification - object:_parentBridge - userInfo:userInfo]; - - } else { - - [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { - - if (!loadError) { - - /** - * Register the display link to start sending js calls after everything - * is setup - */ - NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; - [_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; - - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:_parentBridge - userInfo:@{ @"bridge": self }]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:[loadError localizedDescription] - withDetails:[loadError localizedFailureReason]]; - } - }]; - } - }]; - } -} - -- (NSDictionary *)modules -{ - RCTAssert(!self.isValid || _modulesByName != nil, @"Bridge modules have not yet been initialized. " - "You may be trying to access a module too early in the startup procedure."); - - return _modulesByName; -} - -#pragma mark - RCTInvalidating - -- (void)invalidate -{ - if (!self.isValid) { - return; - } - - RCTAssertMainThread(); - - _valid = NO; - if (_latestJSExecutor == _javaScriptExecutor) { - _latestJSExecutor = nil; - } - - void (^mainThreadInvalidate)(void) = ^{ - RCTAssertMainThread(); - - [_mainDisplayLink invalidate]; - _mainDisplayLink = nil; - - // Invalidate modules - dispatch_group_t group = dispatch_group_create(); - for (id target in _modulesByID.allObjects) { - if ([target respondsToSelector:@selector(invalidate)]) { - [self dispatchBlock:^{ - [(id)target invalidate]; - } forModule:target dispatchGroup:group]; - } - _queuesByID[RCTModuleIDsByName[RCTBridgeModuleNameForClass([target class])]] = nil; - } - dispatch_group_notify(group, dispatch_get_main_queue(), ^{ - _queuesByID = nil; - _modulesByID = nil; - _modulesByName = nil; - _frameUpdateObservers = nil; - }); - }; - - if (!_javaScriptExecutor) { - - // No JS thread running - mainThreadInvalidate(); - return; - } - - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - - /** - * JS Thread deallocations - */ - [_javaScriptExecutor invalidate]; - _javaScriptExecutor = nil; - - [_jsDisplayLink invalidate]; - _jsDisplayLink = nil; - - /** - * Main Thread deallocations - */ - dispatch_async(dispatch_get_main_queue(), mainThreadInvalidate); - - }]; -} - -#pragma mark - RCTBridge methods - -/** - * Public. Can be invoked from any thread. - */ -- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args -{ - NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; - - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[ids[0], ids[1], args ?: @[]] - context:RCTGetExecutorID(_javaScriptExecutor)]; -} - -/** - * Private hack to support `setTimeout(fn, 0)` - */ -- (void)_immediatelyCallTimer:(NSNumber *)timer -{ - RCTAssertJSThread(); - - dispatch_block_t block = ^{ - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[@"JSTimersExecution", @"callTimers", @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - }; - - if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { - [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; - } else { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } -} - -- (void)enqueueApplicationScript:(NSString *)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; - } - - RCTProfileBeginEvent(); - NSNumber *context = RCTGetExecutorID(_javaScriptExecutor); - [_javaScriptExecutor executeJSCall:@"BatchedBridge" - method:@"flushedQueue" - arguments:@[] - context:context - callback:^(id json, NSError *error) { - RCTProfileEndEvent(@"FetchApplicationScriptCallbacks", @"js_call,init", @{ - @"json": RCTNullIfNil(json), - @"error": RCTNullIfNil(error), - }); - - [self _handleBuffer:json context:context]; - - onComplete(error); - }]; - }]; -} - -#pragma mark - Payload Generation -- (void)dispatchBlock:(dispatch_block_t)block - forModule:(id)module -{ - [self dispatchBlock:block forModule:module dispatchGroup:NULL]; -} - -- (void)dispatchBlock:(dispatch_block_t)block - forModule:(id)module - dispatchGroup:(dispatch_group_t)group -{ - [self dispatchBlock:block - forModuleID:RCTModuleIDsByName[RCTBridgeModuleNameForClass([module class])] - dispatchGroup:group]; -} - -- (void)dispatchBlock:(dispatch_block_t)block - forModuleID:(NSNumber *)moduleID -{ - [self dispatchBlock:block forModuleID:moduleID dispatchGroup:NULL]; -} - -- (void)dispatchBlock:(dispatch_block_t)block - forModuleID:(NSNumber *)moduleID - dispatchGroup:(dispatch_group_t)group -{ - RCTAssertJSThread(); - - id queue = nil; - if (moduleID) { - queue = _queuesByID[moduleID]; - } - - if (queue == RCTJSThread) { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } else if (queue) { - if (group != NULL) { - dispatch_group_async(group, queue, block); - } else { - dispatch_async(queue, block); - } - } -} - -/** - * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer, - * on the JS thread, but only in non-batched mode. - */ -- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context -{ - /** - * AnyThread - */ - - RCTProfileBeginFlowEvent(); - - __weak RCTBatchedBridge *weakSelf = self; - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); - - RCTBatchedBridge *strongSelf = weakSelf; - if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { - return; - } - - - RCT_IF_DEV(NSNumber *callID = _RCTProfileBeginFlowEvent();) - id call = @{ - @"js_args": @{ - @"module": module, - @"method": method, - @"args": args, - }, - @"context": context ?: @0, - RCT_IF_DEV(@"call_id": callID,) - }; - if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - strongSelf->_scheduledCallbacks[args[0]] = call; - } else { - [strongSelf->_scheduledCalls addObject:call]; - } - - RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); - }]; -} - -- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context -{ - RCTAssertJSThread(); - - [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; - - RCTJavaScriptCallback processResponse = ^(id json, __unused NSError *error) { - if (!self.isValid) { - return; - } - [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; - [self _handleBuffer:json context:context]; - }; - - [_javaScriptExecutor executeJSCall:module - method:method - arguments:args - context:context - callback:processResponse]; -} - -#pragma mark - Payload Processing - -- (void)_handleBuffer:(id)buffer context:(NSNumber *)context -{ - RCTAssertJSThread(); - - if (buffer == nil || buffer == (id)kCFNull) { - return; - } - - NSArray *requestsArray = [RCTConvert NSArray:buffer]; - -#if RCT_DEBUG - - if (![buffer isKindOfClass:[NSArray class]]) { - RCTLogError(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class])); - return; - } - - for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) { - id field = [requestsArray objectAtIndex:fieldIndex]; - if (![field isKindOfClass:[NSArray class]]) { - RCTLogError(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class])); - return; - } - } - -#endif - - NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; - NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; - NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; - - NSUInteger numRequests = [moduleIDs count]; - - if (RCT_DEBUG && (numRequests != methodIDs.count || numRequests != paramsArrays.count)) { - RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); - return; - } - - NSMapTable *buckets = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:_queuesByID.count]; - for (NSUInteger i = 0; i < numRequests; i++) { - id queue = RCTNullIfNil(_queuesByID[moduleIDs[i]]); - NSMutableOrderedSet *set = [buckets objectForKey:queue]; - if (!set) { - set = [[NSMutableOrderedSet alloc] init]; - [buckets setObject:set forKey:queue]; - } - [set addObject:@(i)]; - } - - for (id queue in buckets) { - RCTProfileBeginFlowEvent(); - dispatch_block_t block = ^{ - RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); - - 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] - context:context]; - } - } - - RCTProfileEndEvent(RCTCurrentThreadName(), @"objc_call,dispatch_async", @{ @"calls": @(calls.count) }); - }; - - if (queue == RCTJSThread) { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } else if (queue) { - dispatch_async(queue, block); - } - } - - // TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case? - [_modulesByID enumerateObjectsUsingBlock: - ^(id module, NSNumber *moduleID, __unused BOOL *stop) { - if ([module respondsToSelector:@selector(batchDidComplete)]) { - [self dispatchBlock:^{ - [module batchDidComplete]; - } forModuleID:moduleID]; - } - }]; -} - -- (BOOL)_handleRequestNumber:(NSUInteger)i - moduleID:(NSUInteger)moduleID - methodID:(NSUInteger)methodID - params:(NSArray *)params - context:(NSNumber *)context -{ - if (!self.isValid) { - return NO; - } - - if (RCT_DEBUG && ![params isKindOfClass:[NSArray class]]) { - RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); - return NO; - } - - // Look up method - NSArray *methods = RCTExportedMethodsByModuleID()[moduleID]; - - if (RCT_DEBUG && methodID >= methods.count) { - RCTLogError(@"Unknown methodID: %zd for module: %zd (%@)", methodID, moduleID, RCTModuleNamesByID[moduleID]); - return NO; - } - - RCTProfileBeginEvent(); - - RCTModuleMethod *method = methods[methodID]; - - // Look up module - id module = self->_modulesByID[moduleID]; - if (RCT_DEBUG && !module) { - RCTLogError(@"No module found for name '%@'", RCTModuleNamesByID[moduleID]); - return NO; - } - - @try { - [method invokeWithBridge:self module:module arguments:params context:context]; - } - @catch (NSException *exception) { - RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); - if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - @throw exception; - } - } - - RCTProfileEndEvent(@"Invoke callback", @"objc_call", @{ - @"module": method.moduleClassName, - @"method": method.JSMethodName, - @"selector": NSStringFromSelector(method.selector), - @"args": RCTJSONStringify(RCTNullIfNil(params), NULL), - }); - - return YES; -} - -- (void)_jsThreadUpdate:(CADisplayLink *)displayLink -{ - RCTAssertJSThread(); - RCTProfileBeginEvent(); - - RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; - for (id observer in _frameUpdateObservers) { - if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { - RCT_IF_DEV(NSString *name = [NSString stringWithFormat:@"[%@ didUpdateFrame:%f]", observer, displayLink.timestamp];) - RCTProfileBeginFlowEvent(); - [self dispatchBlock:^{ - RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); - [observer didUpdateFrame:frameUpdate]; - RCTProfileEndEvent(name, @"objc_call,fps", nil); - } forModule:(id)observer]; - } - } - - NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; - NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); - calls = [calls filteredArrayUsingPredicate: - [NSPredicate predicateWithBlock: - ^BOOL(NSDictionary *call, __unused NSDictionary *bindings) { - return [call[@"context"] isEqualToNumber:currentExecutorID]; - }]]; - - RCT_IF_DEV( - RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); - - for (NSDictionary *call in calls) { - _RCTProfileEndFlowEvent(call[@"call_id"]); - } - ) - - if (calls.count > 0) { - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"processBatch" - arguments:@[[calls valueForKey:@"js_args"]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - } - - RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); - - dispatch_async(dispatch_get_main_queue(), ^{ - [self.perfStats.jsGraph tick:displayLink.timestamp]; - }); -} - -- (void)_mainThreadUpdate:(CADisplayLink *)displayLink -{ - RCTAssertMainThread(); - - RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); - - [self.perfStats.uiGraph tick:displayLink.timestamp]; -} - -- (void)startProfiling -{ - RCTAssertMainThread(); - - if (![_parentBridge.bundleURL.scheme isEqualToString:@"http"]) { - RCTLogError(@"To run the profiler you must be running from the dev server"); - return; - } - - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - RCTProfileInit(self); - }]; -} - -- (void)stopProfiling -{ - RCTAssertMainThread(); - - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - NSString *log = RCTProfileEnd(self); - NSURL *bundleURL = _parentBridge.bundleURL; - NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; - NSURL *URL = [NSURL URLWithString:URLString]; - NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL]; - URLRequest.HTTPMethod = @"POST"; - [URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSURLSessionTask *task = - [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest - fromData:[log dataUsingEncoding:NSUTF8StringEncoding] - completionHandler: - ^(__unused NSData *data, __unused NSURLResponse *response, NSError *error) { - if (error) { - RCTLogError(@"%@", error.localizedDescription); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [[[UIAlertView alloc] initWithTitle:@"Profile" - message:@"The profile has been generated, check the dev server log for instructions." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; - }); - } - }]; - - [task resume]; - }]; -} - -@end diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 4715f9df7..e9823a3d7 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -78,7 +78,7 @@ extern dispatch_queue_t RCTJSThread; * and the bridge will populate the methodQueue property for you automatically * when it initializes the module. */ -@property (nonatomic, weak, readonly) dispatch_queue_t methodQueue; +@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue; /** * Place this macro in your class implementation to automatically register diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 228447280..8461db3e9 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -635,17 +635,24 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ return nil; } - if (RCT_DEBUG && ![json isKindOfClass:[NSString class]]) { + if (RCT_DEBUG && ![json isKindOfClass:[NSString class]] && ![json isKindOfClass:[NSDictionary class]]) { RCTLogConvertError(json, "an image"); return nil; } - if ([json length] == 0) { - return nil; + UIImage *image; + NSString *path; + CGFloat scale = 0.0; + if ([json isKindOfClass:[NSString class]]) { + if ([json length] == 0) { + return nil; + } + path = json; + } else { + path = [self NSString:json[@"uri"]]; + scale = [self CGFloat:json[@"scale"]]; } - UIImage *image = nil; - NSString *path = json; if ([path hasPrefix:@"data:"]) { NSURL *url = [NSURL URLWithString:path]; NSData *imageData = [NSData dataWithContentsOfURL:url]; @@ -658,6 +665,11 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:path ofType:nil]]; } } + + if (scale > 0) { + image = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; + } + // NOTE: we don't warn about nil images because there are legitimate // case where we find out if a string is an image by using this method return image; diff --git a/React/Base/RCTFrameUpdate.h b/React/Base/RCTFrameUpdate.h index b9a3d993f..f14bd5b86 100644 --- a/React/Base/RCTFrameUpdate.h +++ b/React/Base/RCTFrameUpdate.h @@ -7,6 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + +@class CADisplayLink; + /** * Interface containing the information about the last screen refresh. */ @@ -22,6 +26,8 @@ */ @property (nonatomic, readonly) NSTimeInterval deltaTime; +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink NS_DESIGNATED_INITIALIZER; + @end /** diff --git a/React/Base/RCTFrameUpdate.m b/React/Base/RCTFrameUpdate.m new file mode 100644 index 000000000..2d9f63825 --- /dev/null +++ b/React/Base/RCTFrameUpdate.m @@ -0,0 +1,29 @@ +/** + * 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 + +#import "RCTFrameUpdate.h" + +#import "RCTUtils.h" + +@implementation RCTFrameUpdate + +RCT_NOT_IMPLEMENTED(-init) + +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +{ + if ((self = [super init])) { + _timestamp = displayLink.timestamp; + _deltaTime = displayLink.duration; + } + return self; +} + +@end diff --git a/React/Base/RCTModuleData.h b/React/Base/RCTModuleData.h new file mode 100644 index 000000000..a9ecbb06e --- /dev/null +++ b/React/Base/RCTModuleData.h @@ -0,0 +1,35 @@ +/** + * 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 + +#import "RCTJavaScriptExecutor.h" + +@interface RCTModuleData : NSObject + +@property (nonatomic, weak, readonly) id javaScriptExecutor; +@property (nonatomic, strong, readonly) NSNumber *uid; +@property (nonatomic, strong, readonly) id instance; + +@property (nonatomic, strong, readonly) Class cls; +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSArray *methods; +@property (nonatomic, copy, readonly) NSDictionary *config; + +@property (nonatomic, strong) dispatch_queue_t queue; + +- (instancetype)initWithExecutor:(id)javaScriptExecutor + uid:(NSNumber *)uid + instance:(id)instance NS_DESIGNATED_INITIALIZER; + + +- (void)dispatchBlock:(dispatch_block_t)block; +- (void)dispatchBlock:(dispatch_block_t)block dispatchGroup:(dispatch_group_t)group; + +@end diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m new file mode 100644 index 000000000..651b8b684 --- /dev/null +++ b/React/Base/RCTModuleData.m @@ -0,0 +1,146 @@ +/** + * 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 "RCTModuleData.h" + +#import "RCTBridge.h" +#import "RCTModuleMethod.h" +#import "RCTLog.h" + +@implementation RCTModuleData + +- (instancetype)initWithExecutor:(id)javaScriptExecutor + uid:(NSNumber *)uid + instance:(id)instance +{ + if ((self = [super init])) { + _javaScriptExecutor = javaScriptExecutor; + _uid = uid; + _instance = instance; + _cls = [instance class]; + _name = RCTBridgeModuleNameForClass(_cls); + + [self loadMethods]; + [self generateConfig]; + [self setQueue]; + } + return self; +} + +RCT_NOT_IMPLEMENTED(-init); + +- (void)loadMethods +{ + NSMutableArray *moduleMethods = [[NSMutableArray alloc] init]; + unsigned int methodCount; + Method *methods = class_copyMethodList(object_getClass(_cls), &methodCount); + + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL selector = method_getName(method); + if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { + IMP imp = method_getImplementation(method); + NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_cls, selector); + RCTModuleMethod *moduleMethod = + [[RCTModuleMethod alloc] initWithObjCMethodName:entries[1] + JSMethodName:entries[0] + moduleClass:_cls]; + + [moduleMethods addObject:moduleMethod]; + } + } + + free(methods); + + _methods = [moduleMethods copy]; +} + +- (void)generateConfig +{ + NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; + config[@"moduleID"] = _uid; + config[@"methods"] = [[NSMutableDictionary alloc] init]; + + if ([_instance respondsToSelector:@selector(constantsToExport)]) { + id consts = [_instance constantsToExport]; + if (consts) { + config[@"constants"] = consts; + } + } + + [_methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger idx, __unused BOOL *stop) { + config[@"methods"][method.JSMethodName] = @{ + @"methodID": @(idx), + @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", + }; + }]; + + _config = [config copy]; +} + +- (void)setQueue +{ + dispatch_queue_t queue = nil; + BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)]; + if (implementsMethodQueue) { + queue = [_instance methodQueue]; + } + if (!queue) { + + // Need to cache queueNames because they aren't retained by dispatch_queue + static NSMutableDictionary *queueNames; + if (!queueNames) { + queueNames = [[NSMutableDictionary alloc] init]; + } + NSString *queueName = queueNames[_name]; + if (!queueName) { + queueName = [NSString stringWithFormat:@"com.facebook.React.%@Queue", _name]; + queueNames[_name] = queueName; + } + + // Create new queue + queue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL); + + // assign it to the module + if (implementsMethodQueue) { + @try { + [(id)_instance setValue:queue forKey:@"methodQueue"]; + } + @catch (NSException *exception) { + RCTLogError(@"%@ is returning nil for it's methodQueue, which is not " + "permitted. You must either return a pre-initialized " + "queue, or @synthesize the methodQueue to let the bridge " + "create a queue for you.", _name); + } + } + } + + _queue = queue; +} + +- (void)dispatchBlock:(dispatch_block_t)block +{ + [self dispatchBlock:block dispatchGroup:NULL]; +} + +- (void)dispatchBlock:(dispatch_block_t)block + dispatchGroup:(dispatch_group_t)group +{ + if (_queue == RCTJSThread) { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + } else if (_queue) { + if (group != NULL) { + dispatch_group_async(group, _queue, block); + } else { + dispatch_async(_queue, block); + } + } +} + +@end diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h new file mode 100644 index 000000000..ffaa22ef7 --- /dev/null +++ b/React/Base/RCTModuleMethod.h @@ -0,0 +1,35 @@ +/** + * 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 + +@class RCTBridge; + +typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { + RCTJavaScriptFunctionKindNormal, + RCTJavaScriptFunctionKindAsync, +}; + +@interface RCTModuleMethod : NSObject + +@property (nonatomic, copy, readonly) NSString *moduleClassName; +@property (nonatomic, copy, readonly) NSString *JSMethodName; +@property (nonatomic, assign, readonly) SEL selector; +@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; + +- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName + JSMethodName:(NSString *)JSMethodName + moduleClass:(Class)moduleClass NS_DESIGNATED_INITIALIZER; + +- (void)invokeWithBridge:(RCTBridge *)bridge + module:(id)module + arguments:(NSArray *)arguments + context:(NSNumber *)context; + +@end diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m new file mode 100644 index 000000000..aca8267b9 --- /dev/null +++ b/React/Base/RCTModuleMethod.m @@ -0,0 +1,288 @@ +/** + * 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 "RCTModuleMethod.h" + +#import + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +@interface RCTBridge (RCTModuleMethod) + +- (void)_invokeAndProcessModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args; + +@end + +@implementation RCTModuleMethod +{ + Class _moduleClass; + SEL _selector; + NSMethodSignature *_methodSignature; + NSArray *_argumentBlocks; +} + +- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName + JSMethodName:(NSString *)JSMethodName + moduleClass:(Class)moduleClass +{ + if ((self = [super init])) { + static NSRegularExpression *typeRegex; + static NSRegularExpression *selectorRegex; + if (!typeRegex) { + NSString *unusedPattern = @"(?:(?:__unused|__attribute__\\(\\(unused\\)\\)))"; + NSString *constPattern = @"(?:const)"; + NSString *constUnusedPattern = [NSString stringWithFormat:@"(?:(?:%@|%@)\\s*)", unusedPattern, constPattern]; + NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", constUnusedPattern]; + typeRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; + + selectorRegex = [[NSRegularExpression alloc] initWithPattern:@"(?<=:).*?(?=[a-zA-Z_]+:|$)" options:0 error:NULL]; + } + + NSMutableArray *argumentNames = [NSMutableArray array]; + [typeRegex enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) { + NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]]; + [argumentNames addObject:argumentName]; + }]; + + // Remove the parameters' type and name + objCMethodName = [selectorRegex stringByReplacingMatchesInString:objCMethodName + options:0 + range:NSMakeRange(0, objCMethodName.length) + withTemplate:@""]; + // Remove any spaces since `selector : (Type)name` is a valid syntax + objCMethodName = [objCMethodName stringByReplacingOccurrencesOfString:@" " withString:@""]; + + _moduleClass = moduleClass; + _moduleClassName = NSStringFromClass(_moduleClass); + _selector = NSSelectorFromString(objCMethodName); + _JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({ + NSString *methodName = NSStringFromSelector(_selector); + NSRange colonRange = [methodName rangeOfString:@":"]; + if (colonRange.length) { + methodName = [methodName substringToIndex:colonRange.location]; + } + methodName; + }); + + + // Get method signature + _methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; + + // Process arguments + NSUInteger numberOfArguments = _methodSignature.numberOfArguments; + NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; + +#define RCT_ARG_BLOCK(_logic) \ + [argumentBlocks addObject:^(__unused RCTBridge *bridge, __unused NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { \ + _logic \ + [invocation setArgument:&value atIndex:index]; \ + }]; \ + + void (^addBlockArgument)(void) = ^{ + RCT_ARG_BLOCK( + + if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing id value = (json ? ^(NSArray *args) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, args]]; + } : ^(__unused NSArray *unused) {}); + ) + }; + + void (^defaultCase)(const char *) = ^(const char *argumentType) { + static const char *blockType = @encode(typeof(^{})); + if (!strcmp(argumentType, blockType)) { + addBlockArgument(); + } else { + RCT_ARG_BLOCK( id value = json; ) + } + }; + + for (NSUInteger i = 2; i < numberOfArguments; i++) { + const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; + + NSString *argumentName = argumentNames[i - 2]; + SEL selector = NSSelectorFromString([argumentName stringByAppendingString:@":"]); + if ([RCTConvert respondsToSelector:selector]) { + switch (argumentType[0]) { + +#define RCT_CONVERT_CASE(_value, _type) \ +case _value: { \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ + RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ + break; \ +} + + RCT_CONVERT_CASE(':', SEL) + RCT_CONVERT_CASE('*', const char *) + RCT_CONVERT_CASE('c', char) + RCT_CONVERT_CASE('C', unsigned char) + RCT_CONVERT_CASE('s', short) + RCT_CONVERT_CASE('S', unsigned short) + RCT_CONVERT_CASE('i', int) + RCT_CONVERT_CASE('I', unsigned int) + RCT_CONVERT_CASE('l', long) + RCT_CONVERT_CASE('L', unsigned long) + RCT_CONVERT_CASE('q', long long) + RCT_CONVERT_CASE('Q', unsigned long long) + RCT_CONVERT_CASE('f', float) + RCT_CONVERT_CASE('d', double) + RCT_CONVERT_CASE('B', BOOL) + RCT_CONVERT_CASE('@', id) + RCT_CONVERT_CASE('^', void *) + + case '{': { + [argumentBlocks addObject:^(__unused RCTBridge *bridge, __unused NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { + NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + void *returnValue = malloc(methodSignature.methodReturnLength); + NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [_invocation setTarget:[RCTConvert class]]; + [_invocation setSelector:selector]; + [_invocation setArgument:&json atIndex:2]; + [_invocation invoke]; + [_invocation getReturnValue:returnValue]; + + [invocation setArgument:returnValue atIndex:index]; + + free(returnValue); + }]; + break; + } + + default: + defaultCase(argumentType); + } + } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { + addBlockArgument(); + } else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) { + RCTAssert(i == numberOfArguments - 2, + @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", + _moduleClassName, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseResolveBlock value = (^(id result) { + NSArray *arguments = result ? @[result] : @[]; + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, arguments]]; + }); + ) + _functionKind = RCTJavaScriptFunctionKindAsync; + } else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) { + RCTAssert(i == numberOfArguments - 1, + @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", + _moduleClassName, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { + NSDictionary *errorJSON = RCTJSErrorFromNSError(error); + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[errorJSON]]]; + }); + ) + _functionKind = RCTJavaScriptFunctionKindAsync; + } else { + + // Unknown argument type + RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" + " to support this type.", argumentName, [self methodName]); + } + } + + _argumentBlocks = [argumentBlocks copy]; + } + + return self; +} + +- (void)invokeWithBridge:(RCTBridge *)bridge + module:(id)module + arguments:(NSArray *)arguments + context:(NSNumber *)context +{ + if (RCT_DEBUG) { + + // Sanity check + RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ + %@ on a module of class %@", [self methodName], [module class]); + + // Safety check + if (arguments.count != _argumentBlocks.count) { + NSInteger actualCount = arguments.count; + NSInteger expectedCount = _argumentBlocks.count; + + // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions + if (_functionKind == RCTJavaScriptFunctionKindAsync) { + actualCount -= 2; + expectedCount -= 2; + } + + RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", + RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, + actualCount, expectedCount); + return; + } + } + + // Create invocation (we can't re-use this as it wouldn't be thread-safe) + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:_methodSignature]; + [invocation setArgument:&_selector atIndex:1]; + [invocation retainArguments]; + + // Set arguments + NSUInteger index = 0; + for (id json in arguments) { + id arg = RCTNilIfNull(json); + void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; + block(bridge, context, invocation, index + 2, arg); + index++; + } + + // Invoke method + [invocation invokeWithTarget:module]; +} + +- (NSString *)methodName +{ + return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass, + NSStringFromSelector(_selector)]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@();>", + [self class], self, [self methodName], _JSMethodName]; +} + +@end diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index d81d545f2..62a17fe7b 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -18,6 +18,7 @@ #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTDefines.h" +#import "RCTModuleData.h" #import "RCTUtils.h" NSString *const RCTProfileDidStartProfiling = @"RCTProfileDidStartProfiling"; @@ -98,12 +99,6 @@ NSDictionary *RCTProfileGetMemoryUsage(void) #pragma mark - Module hooks -@interface RCTBridge (Private) - -- (void)dispatchBlock:(dispatch_block_t)block forModule:(id)module; - -@end - static const char *RCTProfileProxyClassName(Class); static const char *RCTProfileProxyClassName(Class class) { @@ -152,9 +147,9 @@ static IMP RCTProfileMsgForward(NSObject *self, SEL selector) static void RCTProfileHookModules(RCTBridge *); static void RCTProfileHookModules(RCTBridge *bridge) { - for (id module in bridge.modules.allValues) { - [bridge dispatchBlock:^{ - Class moduleClass = object_getClass(module); + for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) { + [moduleData dispatchBlock:^{ + Class moduleClass = moduleData.cls; Class proxyClass = objc_allocateClassPair(moduleClass, RCTProfileProxyClassName(moduleClass), 0); unsigned int methodCount; @@ -167,7 +162,7 @@ static void RCTProfileHookModules(RCTBridge *bridge) } IMP originalIMP = method_getImplementation(method); const char *returnType = method_getTypeEncoding(method); - class_addMethod(proxyClass, selector, RCTProfileMsgForward(module, selector), returnType); + class_addMethod(proxyClass, selector, RCTProfileMsgForward(moduleData.instance, selector), returnType); class_addMethod(proxyClass, RCTProfileProxySelector(selector), originalIMP, returnType); } free(methods); @@ -185,24 +180,24 @@ static void RCTProfileHookModules(RCTBridge *bridge) } objc_registerClassPair(proxyClass); - object_setClass(module, proxyClass); - } forModule:module]; + object_setClass(moduleData.instance, proxyClass); + }]; } } void RCTProfileUnhookModules(RCTBridge *); void RCTProfileUnhookModules(RCTBridge *bridge) { - for (id module in bridge.modules.allValues) { - [bridge dispatchBlock:^{ - RCTProfileLock( - Class proxyClass = object_getClass(module); - if (module.class != proxyClass) { - object_setClass(module, module.class); - objc_disposeClassPair(proxyClass); - } - ); - } forModule:module]; + for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) { + [moduleData dispatchBlock:^{ + RCTProfileLock( + Class proxyClass = object_getClass(moduleData.instance); + if (moduleData.cls != proxyClass) { + object_setClass(moduleData.instance, moduleData.cls); + objc_disposeClassPair(proxyClass); + } + ); + }]; }; } diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 48eb903ae..b7f0fc029 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -61,3 +61,5 @@ RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); // Convert nil values to NSNull, and vice-versa RCT_EXTERN id RCTNullIfNil(id value); RCT_EXTERN id RCTNilIfNull(id value); + +RCT_EXTERN NSDictionary *RCTJSErrorFromNSError(NSError *error); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 15b5f8f7a..7de19b51b 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -285,3 +285,24 @@ id RCTNilIfNull(id value) { return value == (id)kCFNull ? nil : value; } + +// TODO: Can we just replace RCTMakeError with this function instead? +NSDictionary *RCTJSErrorFromNSError(NSError *error) +{ + NSString *errorMessage; + NSArray *stackTrace = [NSThread callStackSymbols]; + NSMutableDictionary *errorInfo = + [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; + + if (error) { + errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; + errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; + errorInfo[@"code"] = @(error.code); + } else { + errorMessage = @"Unknown error from a native module"; + errorInfo[@"domain"] = RCTErrorDomain; + errorInfo[@"code"] = @-1; + } + + return RCTMakeError(errorMessage, nil, errorInfo); +} diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 2bec93c9a..c6855fbf0 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -810,7 +810,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag viewName:(NSString *)viewName - rootTag:(NSNumber *)rootTag + rootTag:(__unused NSNumber *)rootTag props:(NSDictionary *)props) { RCTViewManager *manager = _viewManagers[viewName]; @@ -1222,7 +1222,7 @@ RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag * this in order to determine if scrolling is appropriate. */ RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag - blockNativeResponder:(BOOL)blockNativeResponder) + blockNativeResponder:(__unused BOOL)blockNativeResponder) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 6d10e2978..51f02e7b2 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -52,6 +52,10 @@ 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 146459251B06C49500B389AA /* RCTFPSGraph.m */; }; + 14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */; }; + 14C2CA741B3AC64300E6CBB2 /* RCTModuleData.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.m */; }; + 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA751B3AC64F00E6CBB2 /* RCTFrameUpdate.m */; }; + 14C2CA781B3ACB0400E6CBB2 /* RCTBatchedBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */; }; 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; @@ -187,6 +191,12 @@ 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapManager.m; sourceTree = ""; }; 146459241B06C49500B389AA /* RCTFPSGraph.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTFPSGraph.h; sourceTree = ""; }; 146459251B06C49500B389AA /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; + 14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModuleMethod.h; sourceTree = ""; }; + 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethod.m; sourceTree = ""; }; + 14C2CA721B3AC64300E6CBB2 /* RCTModuleData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModuleData.h; sourceTree = ""; }; + 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleData.m; sourceTree = ""; }; + 14C2CA751B3AC64F00E6CBB2 /* RCTFrameUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFrameUpdate.m; sourceTree = ""; }; + 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBatchedBridge.m; sourceTree = ""; }; 14F362071AABD06A001CE568 /* RCTSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitch.h; sourceTree = ""; }; 14F362081AABD06A001CE568 /* RCTSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitch.m; sourceTree = ""; }; 14F362091AABD06A001CE568 /* RCTSwitchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitchManager.h; sourceTree = ""; }; @@ -444,6 +454,12 @@ 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, 142014171B32094000CC17BA /* RCTPerformanceLogger.m */, 142014181B32094000CC17BA /* RCTPerformanceLogger.h */, + 14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */, + 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */, + 14C2CA721B3AC64300E6CBB2 /* RCTModuleData.h */, + 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.m */, + 14C2CA751B3AC64F00E6CBB2 /* RCTFrameUpdate.m */, + 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */, ); path = Base; sourceTree = ""; @@ -526,12 +542,14 @@ 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */, 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */, 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, + 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, + 14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */, 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */, @@ -554,8 +572,10 @@ 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */, 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */, 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */, + 14C2CA781B3ACB0400E6CBB2 /* RCTBatchedBridge.m in Sources */, 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */, 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */, + 14C2CA741B3AC64300E6CBB2 /* RCTModuleData.m in Sources */, 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */, 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */, 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */, diff --git a/React/Views/RCTTabBarItem.h b/React/Views/RCTTabBarItem.h index 0e2433c3c..8fe6d8efb 100644 --- a/React/Views/RCTTabBarItem.h +++ b/React/Views/RCTTabBarItem.h @@ -11,7 +11,7 @@ @interface RCTTabBarItem : UIView -@property (nonatomic, copy) NSString *icon; +@property (nonatomic, copy) id icon; @property (nonatomic, assign, getter=isSelected) BOOL selected; @property (nonatomic, readonly) UITabBarItem *barItem; diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index e6caa0b18..6855c1410 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -25,7 +25,7 @@ return _barItem; } -- (void)setIcon:(NSString *)icon +- (void)setIcon:(id)icon { static NSDictionary *systemIcons; static dispatch_once_t onceToken; @@ -54,7 +54,7 @@ UIImage *image = [RCTConvert UIImage:_icon]; UITabBarItem *oldItem = _barItem; if (image) { - + // Recreate barItem if previous item was a system icon if (wasSystemIcon) { _barItem = nil; diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index cdfa8669c..d1a96ef2a 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -22,7 +22,7 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(selected, BOOL); -RCT_EXPORT_VIEW_PROPERTY(icon, NSString); +RCT_EXPORT_VIEW_PROPERTY(icon, id); RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage); RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString); RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index 3d440665a..cc71b39fa 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -39,6 +39,7 @@ RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL) RCT_REMAP_VIEW_PROPERTY(password, secureTextEntry, BOOL) // backwards compatibility RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType, UITextAutocapitalizationType) +RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment, NSTextAlignment) RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextField) { view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; diff --git a/package.json b/package.json index 7b8e8525e..13c57fbeb 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "absolute-path": "0.0.0", "babel": "5.4.3", + "babel-core": "^5.6.4", "bluebird": "^2.9.21", "chalk": "^1.0.0", "connect": "2.8.3", diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index 09861b522..673d9c58a 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -13,6 +13,8 @@ jest .dontMock('crypto') .dontMock('absolute-path') .dontMock('../docblock') + .dontMock('../../crawlers') + .dontMock('../../crawlers/node') .dontMock('../../replacePatterns') .dontMock('../../../lib/getAssetDataFromName') .dontMock('../../fastfs') @@ -22,6 +24,8 @@ jest .dontMock('../../Package') .dontMock('../../ModuleCache'); +const Promise = require('promise'); + jest.mock('fs'); describe('DependencyGraph', function() { @@ -36,7 +40,8 @@ describe('DependencyGraph', function() { fileWatcher = { on: function() { return this; - } + }, + isWatchman: () => Promise.resolve(false) }; }); diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js index 9390a10e0..8145bfa03 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -8,17 +8,19 @@ */ 'use strict'; -const path = require('path'); +const Activity = require('../../Activity'); +const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); const Fastfs = require('../fastfs'); const ModuleCache = require('../ModuleCache'); -const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); -const declareOpts = require('../../lib/declareOpts'); -const isAbsolutePath = require('absolute-path'); -const debug = require('debug')('DependencyGraph'); -const getAssetDataFromName = require('../../lib/getAssetDataFromName'); -const util = require('util'); const Promise = require('promise'); const _ = require('underscore'); +const crawl = require('../crawlers'); +const debug = require('debug')('DependencyGraph'); +const declareOpts = require('../../lib/declareOpts'); +const getAssetDataFromName = require('../../lib/getAssetDataFromName'); +const isAbsolutePath = require('absolute-path'); +const path = require('path'); +const util = require('util'); const validateOpts = declareOpts({ roots: { @@ -68,13 +70,18 @@ class DependencyGraph { return this._loading; } - const modulePattern = new RegExp( - '\.(' + ['js', 'json'].concat(this._assetExts).join('|') + ')$' - ); + const crawlActivity = Activity.startEvent('fs crawl'); + const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED); + this._crawling = crawl(allRoots, { + ignore: this._opts.ignoreFilePath, + exts: ['js', 'json'].concat(this._opts.assetExts), + fileWatcher: this._opts.fileWatcher, + }); + this._crawling.then((files) => Activity.endEvent(crawlActivity)); this._fastfs = new Fastfs(this._opts.roots,this._opts.fileWatcher, { - pattern: modulePattern, ignore: this._opts.ignoreFilePath, + crawling: this._crawling, }); this._fastfs.on('change', this._processFileChange.bind(this)); @@ -454,14 +461,10 @@ class DependencyGraph { this._assetMap_DEPRECATED = Object.create(null); - const pattern = new RegExp( - '\.(' + this._opts.assetExts.join('|') + ')$' - ); - const fastfs = new Fastfs( this._opts.assetRoots_DEPRECATED, this._opts.fileWatcher, - { pattern, ignore: this._opts.ignoreFilePath } + { ignore: this._opts.ignoreFilePath, crawling: this._crawling } ); fastfs.on('change', this._processAssetChange_DEPRECATED.bind(this)); diff --git a/packager/react-packager/src/DependencyResolver/crawlers/index.js b/packager/react-packager/src/DependencyResolver/crawlers/index.js new file mode 100644 index 000000000..71290af44 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/crawlers/index.js @@ -0,0 +1,36 @@ +'use strict'; + +const nodeCrawl = require('./node'); +//const watchmanCrawl = require('./watchman'); + +function crawl(roots, options) { + return nodeCrawl(roots, options); + + // Although, in theory, watchman should be much faster; + // there is currently a bottleneck somewhere in the + // encoding/decoding that is causing it to be slower + // than node crawling. However, this should be fixed soon. + // https://github.com/facebook/watchman/issues/113 + /* + const {fileWatcher} = options; + return fileWatcher.isWatchman().then(isWatchman => { + + console.log(isWatchman); + if (!isWatchman) { + return false; + } + + // Make sure we're dealing with a version of watchman + // that's using `watch-project` + // TODO(amasad): properly expose (and document) used sane internals. + return fileWatcher.getWatchers().then(([watcher]) => !!watcher.watchProjectInfo.root); + }).then(isWatchman => { + if (isWatchman) { + return watchmanCrawl(roots, options); + } + + return nodeCrawl(roots, options); + });*/ +} + +module.exports = crawl; diff --git a/packager/react-packager/src/DependencyResolver/crawlers/node.js b/packager/react-packager/src/DependencyResolver/crawlers/node.js new file mode 100644 index 000000000..72030905d --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/crawlers/node.js @@ -0,0 +1,61 @@ +'use strict'; + +const Promise = require('promise'); +const debug = require('debug')('DependencyGraph'); +const fs = require('fs'); +const path = require('path'); + +const readDir = Promise.denodeify(fs.readdir); +const stat = Promise.denodeify(fs.stat); + +function nodeRecReadDir(roots, {ignore, exts}) { + const queue = roots.slice(); + const retFiles = []; + const extPattern = new RegExp( + '\.(' + exts.join('|') + ')$' + ); + + function search() { + const currDir = queue.shift(); + if (!currDir) { + return Promise.resolve(); + } + + return readDir(currDir) + .then(files => files.map(f => path.join(currDir, f))) + .then(files => Promise.all( + files.map(f => stat(f).catch(handleBrokenLink)) + ).then(stats => [ + // Remove broken links. + files.filter((file, i) => !!stats[i]), + stats.filter(Boolean), + ])) + .then(([files, stats]) => { + files.forEach((filePath, i) => { + if (ignore(filePath)) { + return; + } + + if (stats[i].isDirectory()) { + queue.push(filePath); + return; + } + + if (filePath.match(extPattern)) { + retFiles.push(filePath); + } + }); + + return search(); + }); + } + + return search().then(() => retFiles); +} + +function handleBrokenLink(e) { + debug('WARNING: error stating, possibly broken symlink', e.message); + return Promise.resolve(); +} + +module.exports = nodeRecReadDir; diff --git a/packager/react-packager/src/DependencyResolver/crawlers/watchman.js b/packager/react-packager/src/DependencyResolver/crawlers/watchman.js new file mode 100644 index 000000000..d6479a513 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/crawlers/watchman.js @@ -0,0 +1,70 @@ +'use strict'; + +const Promise = require('promise'); +const path = require('path'); + +function watchmanRecReadDir(roots, {ignore, fileWatcher, exts}) { + const files = []; + return Promise.all( + roots.map( + root => fileWatcher.getWatcherForRoot(root) + ) + ).then( + watchers => { + // All watchman roots for all watches we have. + const watchmanRoots = watchers.map( + watcher => watcher.watchProjectInfo.root + ); + + // Actual unique watchers (because we use watch-project we may end up with + // duplicate "real" watches, and that's by design). + // TODO(amasad): push this functionality into the `FileWatcher`. + const uniqueWatchers = watchers.filter( + (watcher, i) => watchmanRoots.indexOf(watcher.watchProjectInfo.root) === i + ); + + return Promise.all( + uniqueWatchers.map(watcher => { + const watchedRoot = watcher.watchProjectInfo.root; + + // Build up an expression to filter the output by the relevant roots. + const dirExpr = ['anyof']; + for (let i = 0; i < roots.length; i++) { + const root = roots[i]; + if (isDescendant(watchedRoot, root)) { + dirExpr.push(['dirname', path.relative(watchedRoot, root)]); + } + } + + const cmd = Promise.promisify(watcher.client.command.bind(watcher.client)); + return cmd(['query', watchedRoot, { + 'suffix': exts, + 'expression': ['allof', ['type', 'f'], 'exists', dirExpr], + 'fields': ['name'], + }]).then(resp => { + if ('warning' in resp) { + console.warn('watchman warning: ', resp.warning); + } + + resp.files.forEach(filePath => { + filePath = path.join( + watchedRoot, + filePath + ); + + if (!ignore(filePath)) { + files.push(filePath); + } + return false; + }); + }); + }) + ); + }).then(() => files); +} + +function isDescendant(root, child) { + return path.relative(root, child).indexOf('..') !== 0; +} + +module.exports = watchmanRecReadDir; diff --git a/packager/react-packager/src/DependencyResolver/fastfs.js b/packager/react-packager/src/DependencyResolver/fastfs.js index 0053b14e3..67f7076e1 100644 --- a/packager/react-packager/src/DependencyResolver/fastfs.js +++ b/packager/react-packager/src/DependencyResolver/fastfs.js @@ -4,28 +4,46 @@ const Promise = require('promise'); const {EventEmitter} = require('events'); const _ = require('underscore'); -const debug = require('debug')('DependencyGraph'); const fs = require('fs'); const path = require('path'); -const readDir = Promise.denodeify(fs.readdir); const readFile = Promise.denodeify(fs.readFile); const stat = Promise.denodeify(fs.stat); + const hasOwn = Object.prototype.hasOwnProperty; class Fastfs extends EventEmitter { - constructor(roots, fileWatcher, {ignore, pattern}) { + constructor(roots, fileWatcher, {ignore, crawling}) { super(); this._fileWatcher = fileWatcher; this._ignore = ignore; - this._pattern = pattern; this._roots = roots.map(root => new File(root, { isDir: true })); this._fastPaths = Object.create(null); + this._crawling = crawling; } build() { - const queue = this._roots.slice(); - return this._search(queue).then(() => { + const rootsPattern = new RegExp( + '^(' + this._roots.map(root => escapeRegExp(root.path)).join('|') + ')' + ); + + return this._crawling.then(files => { + files.forEach(filePath => { + if (filePath.match(rootsPattern)) { + const newFile = new File(filePath, { isDir: false }); + const parent = this._fastPaths[path.dirname(filePath)]; + if (parent) { + parent.addChild(newFile); + } else { + this._add(newFile); + for (let file = newFile; file; file = file.parent) { + if (!this._fastPaths[file.path]) { + this._fastPaths[file.path] = file; + } + } + } + } + }); this._fileWatcher.on('all', this._processFileChange.bind(this)); }); } @@ -134,32 +152,6 @@ class Fastfs extends EventEmitter { this._getAndAssertRoot(file.path).addChild(file); } - _search(queue) { - const dir = queue.shift(); - if (!dir) { - return Promise.resolve(); - } - - return readAndStatDir(dir.path).then(([filePaths, stats]) => { - filePaths.forEach((filePath, i) => { - if (this._ignore(filePath)) { - return; - } - - if (stats[i].isDirectory()) { - queue.push( - new File(filePath, { isDir: true, fstat: stats[i] }) - ); - return; - } - - if (filePath.match(this._pattern)) { - this._add(new File(filePath, { fstat: stats[i] })); - } - }); - return this._search(queue); - }); - } _processFileChange(type, filePath, root, fstat) { const absPath = path.join(root, filePath); @@ -182,10 +174,7 @@ class Fastfs extends EventEmitter { delete this._fastPaths[path.normalize(absPath)]; if (type !== 'delete') { - this._add(new File(absPath, { - isDir: false, - fstat - })); + this._add(new File(absPath, { isDir: false })); } this.emit('change', type, filePath, root, fstat); @@ -193,16 +182,12 @@ class Fastfs extends EventEmitter { } class File { - constructor(filePath, {isDir, fstat}) { + constructor(filePath, { isDir }) { this.path = filePath; this.isDir = Boolean(isDir); if (this.isDir) { this.children = Object.create(null); } - - if (fstat) { - this._stat = Promise.resolve(fstat); - } } read() { @@ -290,21 +275,8 @@ function isDescendant(root, child) { return path.relative(root, child).indexOf('..') !== 0; } -function readAndStatDir(dir) { - return readDir(dir) - .then(files => Promise.all(files.map(f => path.join(dir, f)))) - .then(files => Promise.all( - files.map(f => stat(f).catch(handleBrokenLink)) - ).then(stats => [ - // Remove broken links. - files.filter((file, i ) => !!stats[i]), - stats.filter(Boolean), - ])); -} - -function handleBrokenLink(e) { - debug('WARNING: error stating, possibly broken symlink', e.message); - return Promise.resolve(); +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } module.exports = Fastfs; diff --git a/packager/react-packager/src/DependencyResolver/polyfills/prelude.js b/packager/react-packager/src/DependencyResolver/polyfills/prelude.js index 9f4db44e2..a1004b7d4 100644 --- a/packager/react-packager/src/DependencyResolver/polyfills/prelude.js +++ b/packager/react-packager/src/DependencyResolver/polyfills/prelude.js @@ -1,2 +1,5 @@ /* eslint global-strict:0 */ __DEV__ = false; + +/* global __BUNDLE_START_TIME__:true */ +__BUNDLE_START_TIME__ = Date.now(); diff --git a/packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js b/packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js index 26b26a076..14b97faad 100644 --- a/packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js +++ b/packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js @@ -1,2 +1,5 @@ /* eslint global-strict:0 */ __DEV__ = true; + +/* global __BUNDLE_START_TIME__:true */ +__BUNDLE_START_TIME__ = Date.now(); diff --git a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js index fc45205a8..a5eeda3da 100644 --- a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js +++ b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js @@ -33,7 +33,7 @@ describe('FileWatcher', function() { pit('it should get the watcher instance when ready', function() { var fileWatcher = new FileWatcher(['rootDir']); - return fileWatcher._loading.then(function(watchers) { + return fileWatcher.getWatchers().then(function(watchers) { watchers.forEach(function(watcher) { expect(watcher instanceof Watcher).toBe(true); }); @@ -48,7 +48,7 @@ describe('FileWatcher', function() { var fileWatcher = new FileWatcher(['rootDir']); var handler = jest.genMockFn(); fileWatcher.on('all', handler); - return fileWatcher._loading.then(function(){ + return fileWatcher.getWatchers().then(function(){ cb(1, 2, 3, 4); jest.runAllTimers(); expect(handler.mock.calls[0]).toEqual([1, 2, 3, 4]); diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index aac211ad2..46b4667e7 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -8,13 +8,14 @@ */ 'use strict'; -var EventEmitter = require('events').EventEmitter; -var sane = require('sane'); -var Promise = require('promise'); -var util = require('util'); -var exec = require('child_process').exec; +const EventEmitter = require('events').EventEmitter; +const sane = require('sane'); +const Promise = require('promise'); +const exec = require('child_process').exec; -var detectingWatcherClass = new Promise(function(resolve) { +const MAX_WAIT_TIME = 25000; + +const detectingWatcherClass = new Promise(function(resolve) { exec('which watchman', function(err, out) { if (err || out.length === 0) { resolve(sane.NodeWatcher); @@ -24,53 +25,76 @@ var detectingWatcherClass = new Promise(function(resolve) { }); }); -module.exports = FileWatcher; +let inited = false; -var MAX_WAIT_TIME = 25000; +class FileWatcher extends EventEmitter { -// Singleton -var fileWatcher = null; + constructor(rootConfigs) { + if (inited) { + throw new Error('FileWatcher can only be instantiated once'); + } + inited = true; -function FileWatcher(rootConfigs) { - if (fileWatcher) { - // This allows us to optimize watching in the future by merging roots etc. - throw new Error('FileWatcher can only be instantiated once'); + super(); + this._watcherByRoot = Object.create(null); + + this._loading = Promise.all( + rootConfigs.map(createWatcher) + ).then(watchers => { + watchers.forEach((watcher, i) => { + this._watcherByRoot[rootConfigs[i].dir] = watcher; + watcher.on( + 'all', + // args = (type, filePath, root, stat) + (...args) => this.emit('all', ...args) + ); + }); + return watchers; + }); + + this._loading.done(); } - fileWatcher = this; + getWatchers() { + return this._loading; + } - this._loading = Promise.all( - rootConfigs.map(createWatcher) - ).then(function(watchers) { - watchers.forEach(function(watcher) { - watcher.on('all', function(type, filepath, root, stat) { - fileWatcher.emit('all', type, filepath, root, stat); - }); - }); - return watchers; - }); - this._loading.done(); + getWatcherForRoot(root) { + return this._loading.then(() => this._watcherByRoot[root]); + } + + isWatchman() { + return detectingWatcherClass.then( + Watcher => Watcher === sane.WatchmanWatcher + ); + } + + end() { + return this._loading.then( + (watchers) => watchers.map( + watcher => Promise.denodeify(watcher.close).call(watcher) + ) + ); + } + + static createDummyWatcher() { + const ev = new EventEmitter(); + ev.end = function() { + return Promise.resolve(); + }; + return ev; + } } -util.inherits(FileWatcher, EventEmitter); - -FileWatcher.prototype.end = function() { - return this._loading.then(function(watchers) { - watchers.forEach(function(watcher) { - return Promise.denodeify(watcher.close).call(watcher); - }); - }); -}; - function createWatcher(rootConfig) { return detectingWatcherClass.then(function(Watcher) { - var watcher = new Watcher(rootConfig.dir, { + const watcher = new Watcher(rootConfig.dir, { glob: rootConfig.globs, dot: false, }); return new Promise(function(resolve, reject) { - var rejectTimeout = setTimeout(function() { + const rejectTimeout = setTimeout(function() { reject(new Error([ 'Watcher took too long to load', 'Try running `watchman version` from your terminal', @@ -86,10 +110,4 @@ function createWatcher(rootConfig) { }); } -FileWatcher.createDummyWatcher = function() { - var ev = new EventEmitter(); - ev.end = function() { - return Promise.resolve(); - }; - return ev; -}; +module.exports = FileWatcher;