diff --git a/README.md b/README.md index f0c5d7c79..58bd93fe3 100644 --- a/README.md +++ b/README.md @@ -92,27 +92,27 @@ the responder system. # FAQ -Q. How does debugging work? Can I set breakpoints in my JS? +Q. How does debugging work? Can I set breakpoints in my JS? A. We are going to add the ability to use the Chrome developer tools soon. We are very passionate about building the best possible developer experience. -Q. When is this coming to Android/Windows/OS X/etc? +Q. When is this coming to Android/Windows/OS X/etc? A. We're working on Android, and we are excited to release it as soon as we can. We are looking forward to the community helping us target other platforms as well :) -Q. How do I create my own app? +Q. How do I create my own app? A. Copy the entire `Examples/TicTacToe` folder, rename stuff in Xcode, and replace the `TicTacToeApp.js` with your own. Then, in `AppDelegate.m`, update `moduleName` to match your call to `Bundler.registerComponent(, )` at the bottom of your JS file, and update `jsCodeLocation` to match your JS file name and location. -Q. Can I submit my own React Native app to the App Store? +Q. Can I submit my own React Native app to the App Store? A. Not yet, but you will be able to soon. If you build something you want to submit to the App Store, come talk to us ASAP. -Q. How do I deploy to my device? +Q. How do I deploy to my device? A. You can change `localhost` in `AppDelegate.m` to your laptop's IP address and grab the bundle over the same Wi-Fi network. You can also download the bundle that the React packager generates, save it to the file `main.jsbundle`, and add it @@ -120,20 +120,20 @@ as a static resource in your Xcode project. Then set the `jsCodeLocation` in `AppDelegate.m` to point to that file and deploy to your device like you would any other app. -Q. What's up with this private repo? Why aren't you just open sourcing it now? +Q. What's up with this private repo? Why aren't you just open sourcing it now? A. We want input from the React community before we open the floodgates so we can incorporate your feedback, and we also have a bunch more features we want to add to make a more complete offering before we open source. -Q. Do you have to ship a JS runtime with your apps? +Q. Do you have to ship a JS runtime with your apps? A. No, we just use the JavaScriptCore public API that is part of iOS 7 and later. -Q. How do I add more native capabilities? +Q. How do I add more native capabilities? A. React Native is designed to be extensible - come talk to us, we would love to work with you. -Q. Can I reuse existing iOS code? +Q. Can I reuse existing iOS code? A. Yes, React Native is designed to be extensible and allow integration of all sorts of native components, such as `UINavigationController` (available as ``), `MKMapView` (not available yet), or your own custom diff --git a/ReactKit/Base/RCTBridge.h b/ReactKit/Base/RCTBridge.h index 81c68f415..ff1c5d7ad 100644 --- a/ReactKit/Base/RCTBridge.h +++ b/ReactKit/Base/RCTBridge.h @@ -6,33 +6,8 @@ @protocol RCTNativeModule; -@class RCTUIManager; @class RCTEventDispatcher; - -/** - * Functions are the one thing that aren't automatically converted to OBJC - * blocks, according to this revert: http://trac.webkit.org/changeset/144489 - * They must be expressed as `JSValue`s. - * - * But storing callbacks causes reference cycles! - * http://stackoverflow.com/questions/19202248/how-can-i-use-jsmanagedvalue-to-avoid-a-reference-cycle-without-the-jsvalue-gett - * We'll live with the leak for now, but need to clean this up asap: - * Passing a reference to the `context` to the bridge would make it easy to - * execute JS. We can add `JSManagedValue`s to protect against this. The same - * needs to be done in `RCTTiming` and friends. - */ - -/** - * Must be kept in sync with `MessageQueue.js`. - */ -typedef NS_ENUM(NSUInteger, RCTBridgeFields) { - RCTBridgeFieldRequestModuleIDs = 0, - RCTBridgeFieldMethodIDs, - RCTBridgeFieldParamss, - RCTBridgeFieldResponseCBIDs, - RCTBridgeFieldResponseReturnValues, - RCTBridgeFieldFlushDateMillis -}; +@class RCTRootView; /** * Utilities for constructing common response objects. When sending a @@ -59,19 +34,19 @@ static inline NSDictionary *RCTAPIErrorObject(NSString *msg) @interface RCTBridge : NSObject - (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - shadowQueue:(dispatch_queue_t)shadowQueue javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig; - (void)enqueueJSCall:(NSUInteger)moduleID methodID:(NSUInteger)methodID args:(NSArray *)args; - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete; -- (void)enqueueUpdateTimers; -@property (nonatomic, readonly) RCTUIManager *uiManager; @property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; +@property (nonatomic, readonly) dispatch_queue_t shadowQueue; // For use in implementing delegates, which may need to queue responses. - (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)callbackID; +- (void)registerRootView:(RCTRootView *)rootView; + /** * Global logging function will print to both xcode and js debugger consoles. * diff --git a/ReactKit/Base/RCTBridge.m b/ReactKit/Base/RCTBridge.m index 4105960d8..6ea5a037a 100644 --- a/ReactKit/Base/RCTBridge.m +++ b/ReactKit/Base/RCTBridge.m @@ -9,11 +9,34 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTModuleIDs.h" -#import "RCTTiming.h" -#import "RCTUIManager.h" #import "RCTUtils.h" -NSString *RCTModuleName(Class moduleClass) +/** + * Functions are the one thing that aren't automatically converted to OBJC + * blocks, according to this revert: http://trac.webkit.org/changeset/144489 + * They must be expressed as `JSValue`s. + * + * But storing callbacks causes reference cycles! + * http://stackoverflow.com/questions/19202248/how-can-i-use-jsmanagedvalue-to-avoid-a-reference-cycle-without-the-jsvalue-gett + * We'll live with the leak for now, but need to clean this up asap: + * Passing a reference to the `context` to the bridge would make it easy to + * execute JS. We can add `JSManagedValue`s to protect against this. The same + * needs to be done in `RCTTiming` and friends. + */ + +/** + * Must be kept in sync with `MessageQueue.js`. + */ +typedef NS_ENUM(NSUInteger, RCTBridgeFields) { + RCTBridgeFieldRequestModuleIDs = 0, + RCTBridgeFieldMethodIDs, + RCTBridgeFieldParamss, + RCTBridgeFieldResponseCBIDs, + RCTBridgeFieldResponseReturnValues, + RCTBridgeFieldFlushDateMillis +}; + +static NSString *RCTModuleName(Class moduleClass) { if ([moduleClass respondsToSelector:@selector(moduleName)]) { @@ -22,22 +45,11 @@ NSString *RCTModuleName(Class moduleClass) } else { // Default implementation, works in most cases - NSString *className = NSStringFromClass(moduleClass); - - // TODO: be more consistent with naming so that this check isn't needed - if ([moduleClass conformsToProtocol:@protocol(RCTNativeViewModule)]) { - if ([className hasPrefix:@"RCTUI"]) { - className = [className substringFromIndex:@"RCT".length]; - } - if ([className hasSuffix:@"Manager"]) { - className = [className substringToIndex:className.length - @"Manager".length]; - } - } - return className; + return NSStringFromClass(moduleClass); } } -NSDictionary *RCTNativeModuleClasses(void) +static NSDictionary *RCTNativeModuleClasses(void) { static NSMutableDictionary *modules; static dispatch_once_t onceToken; @@ -79,46 +91,30 @@ NSDictionary *RCTNativeModuleClasses(void) { NSMutableDictionary *_moduleInstances; NSDictionary *_javaScriptModulesConfig; - dispatch_queue_t _shadowQueue; - RCTTiming *_timing; id _javaScriptExecutor; } static id _latestJSExecutor; - (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - shadowQueue:(dispatch_queue_t)shadowQueue javaScriptModulesConfig:(NSDictionary *)javaScriptModulesConfig { if ((self = [super init])) { _javaScriptExecutor = javaScriptExecutor; _latestJSExecutor = _javaScriptExecutor; - _shadowQueue = shadowQueue; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - - _moduleInstances = [[NSMutableDictionary alloc] init]; - - // TODO (#5906496): Remove special case - _timing = [[RCTTiming alloc] initWithBridge:self]; _javaScriptModulesConfig = javaScriptModulesConfig; - _moduleInstances[RCTModuleName([RCTTiming class])] = _timing; - - // TODO (#5906496): Remove special case - NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; - [RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { - if ([moduleClass conformsToProtocol:@protocol(RCTNativeViewModule)]) { - viewManagers[moduleName] = [[moduleClass alloc] init]; - } - }]; - _uiManager = [[RCTUIManager alloc] initWithShadowQueue:_shadowQueue viewManagers:viewManagers]; - _uiManager.eventDispatcher = _eventDispatcher; - _moduleInstances[RCTModuleName([RCTUIManager class])] = _uiManager; - [_moduleInstances addEntriesFromDictionary:viewManagers]; - - // Register remaining modules + _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); + + // Register modules + _moduleInstances = [[NSMutableDictionary alloc] init]; [RCTNativeModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { if (_moduleInstances[moduleName] == nil) { - _moduleInstances[moduleName] = [[moduleClass alloc] init]; + if ([moduleClass instancesRespondToSelector:@selector(initWithBridge:)]) { + _moduleInstances[moduleName] = [[moduleClass alloc] initWithBridge:self]; + } else { + _moduleInstances[moduleName] = [[moduleClass alloc] init]; + } } }]; @@ -148,18 +144,16 @@ static id _latestJSExecutor; _javaScriptExecutor = nil; dispatch_sync(_shadowQueue, ^{ - // Make sure all dispatchers have been executed before - // freeing up memory from _asyncHookMapByModuleID + // Make sure all dispatchers have been executed before continuing + // TODO: is this still needed? }); - + for (id target in _moduleInstances.objectEnumerator) { if ([target respondsToSelector:@selector(invalidate)]) { [(id)target invalidate]; } } [_moduleInstances removeAllObjects]; - - _timing = nil; } /** @@ -199,11 +193,6 @@ static id _latestJSExecutor; }]; } -- (void)enqueueUpdateTimers -{ - [_timing enqueueUpdateTimers]; -} - #pragma mark - Payload Generation - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args @@ -274,12 +263,12 @@ static id _latestJSExecutor; } } - NSArray *moduleIDs = [requestsArray objectAtIndex:RCTBridgeFieldRequestModuleIDs]; - NSArray *methodIDs = [requestsArray objectAtIndex:RCTBridgeFieldMethodIDs]; - NSArray *paramss = [requestsArray objectAtIndex:RCTBridgeFieldParamss]; + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; + NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; + NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; NSUInteger numRequests = [moduleIDs count]; - BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramss count]; + BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramsArrays count]; if (!allSame) { RCTLogMustFix(@"Invalid data message - all must be length: %zd", numRequests); return; @@ -288,31 +277,102 @@ static id _latestJSExecutor; for (NSUInteger i = 0; i < numRequests; i++) { @autoreleasepool { [self _handleRequestNumber:i - moduleID:[moduleIDs objectAtIndex:i] - methodID:[methodIDs objectAtIndex:i] - params:[paramss objectAtIndex:i]]; + moduleID:[moduleIDs[i] integerValue] + methodID:[methodIDs[i] integerValue] + params:paramsArrays[i]]; } } - // Update modules - for (id target in _moduleInstances.objectEnumerator) { - if ([target respondsToSelector:@selector(batchDidComplete)]) { - dispatch_async(_shadowQueue, ^{ + // TODO: only used by RCTUIManager - can we eliminate this special case? + dispatch_async(_shadowQueue, ^{ + for (id target in _moduleInstances.objectEnumerator) { + if ([target respondsToSelector:@selector(batchDidComplete)]) { [target batchDidComplete]; - }); + } } - } + }); } -- (void)_handleRequestNumber:(NSUInteger)i moduleID:(id)moduleID methodID:(id)methodID params:(id)params +- (BOOL)_handleRequestNumber:(NSUInteger)i + moduleID:(NSInteger)moduleID + methodID:(NSInteger)methodID + params:(NSArray *)params { - if (![moduleID isKindOfClass:[NSNumber class]] || ![methodID isKindOfClass:[NSNumber class]] || ![params isKindOfClass:[NSArray class]]) { + if (![params isKindOfClass:[NSArray class]]) { RCTLogMustFix(@"Invalid module/method/params tuple for request #%zd", i); - return; + return NO; } - [self _dispatchUsingAsyncHookMapWithModuleID:[moduleID integerValue] - methodID:[methodID integerValue] - params:params]; + + if (moduleID < 0 || moduleID >= RCTExportedMethodsByModule().count) { + return NO; + } + + NSString *moduleName = RCTExportedModuleNameAtSortedIndex(moduleID); + NSArray *methods = RCTExportedMethodsByModule()[moduleName]; + if (methodID < 0 || methodID >= methods.count) { + return NO; + } + + RCTModuleMethod *method = methods[methodID]; + NSUInteger methodArity = method.arity; + if (params.count != methodArity) { + RCTLogMustFix(@"Expected %tu arguments but got %tu invoking %@.%@", + methodArity, + params.count, + moduleName, + method.JSMethodName); + return NO; + } + + __weak RCTBridge *weakSelf = self; + dispatch_async(_shadowQueue, ^{ + __strong RCTBridge *strongSelf = weakSelf; + + if (!strongSelf.isValid) { + // strongSelf has been invalidated since the dispatch_async call and this + // invocation should not continue. + return; + } + + NSInvocation *invocation = [RCTBridge invocationForAdditionalArguments:methodArity]; + + // TODO: we should just store module instances by index, since that's how we look them up anyway + id target = strongSelf->_moduleInstances[moduleName]; + RCTAssert(target != nil, @"No module found for name '%@'", moduleName); + + [invocation setArgument:&target atIndex:0]; + + SEL selector = method.selector; + [invocation setArgument:&selector atIndex:1]; + + // Retain used blocks until after invocation completes. + NSMutableArray *blocks = [NSMutableArray array]; + + [params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) { + if ([param isEqual:[NSNull null]]) { + param = nil; + } else if ([method.blockArgumentIndexes containsIndex:idx]) { + id block = [strongSelf createResponseSenderBlock:[param integerValue]]; + [blocks addObject:block]; + param = block; + } + + [invocation setArgument:¶m atIndex:idx + 2]; + }]; + + @try { + [invocation invoke]; + } + @catch (NSException *exception) { + RCTLogMustFix(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, target, params, exception); + } + @finally { + // Force `blocks` to remain alive until here. + blocks = nil; + } + }); + + return YES; } /** @@ -352,84 +412,6 @@ static id _latestJSExecutor; return invocation; } -- (BOOL)_dispatchUsingAsyncHookMapWithModuleID:(NSInteger)moduleID - methodID:(NSInteger)methodID - params:(NSArray *)params -{ - if (moduleID < 0 || moduleID >= RCTExportedMethodsByModule().count) { - return NO; - } - - NSString *moduleName = RCTExportedModuleNameAtSortedIndex(moduleID); - NSArray *methods = RCTExportedMethodsByModule()[moduleName]; - if (methodID < 0 || methodID >= methods.count) { - return NO; - } - - RCTModuleMethod *method = methods[methodID]; - NSUInteger methodArity = method.arity; - if (params.count != methodArity) { - RCTLogMustFix( - @"Expected %tu arguments but got %tu invoking %@.%@", - methodArity, - params.count, - moduleName, - method.JSMethodName - ); - return NO; - } - - __weak RCTBridge *weakSelf = self; - dispatch_async(_shadowQueue, ^{ - __strong RCTBridge *strongSelf = weakSelf; - - if (!strongSelf.isValid) { - // strongSelf has been invalidated since the dispatch_async call and this - // invocation should not continue. - return; - } - - NSInvocation *invocation = [RCTBridge invocationForAdditionalArguments:methodArity]; - - // TODO: we should just store module instances by index, since that's how we look them up anyway - id target = strongSelf->_moduleInstances[moduleName]; - RCTAssert(target != nil, @"No module found for name '%@'", moduleName); - - [invocation setArgument:&target atIndex:0]; - - SEL selector = method.selector; - [invocation setArgument:&selector atIndex:1]; - - // Retain used blocks until after invocation completes. - NSMutableArray *blocks = [NSMutableArray array]; - - [params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) { - if ([param isEqual:[NSNull null]]) { - param = nil; - } else if ([method.blockArgumentIndexes containsIndex:idx]) { - id block = [strongSelf createResponseSenderBlock:[param integerValue]]; - [blocks addObject:block]; - param = block; - } - - [invocation setArgument:¶m atIndex:idx + 2]; - }]; - - @try { - [invocation invoke]; - } - @catch (NSException *exception) { - RCTLogMustFix(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, target, params, exception); - } - @finally { - // Force `blocks` to remain alive until here. - blocks = nil; - } - }); - - return YES; -} - - (void)doneRegisteringModules { RCTAssertMainThread(); @@ -456,8 +438,7 @@ static id _latestJSExecutor; moduleConfig[@"methods"] = methods; id target = [_moduleInstances objectForKey:moduleName]; - if ([target respondsToSelector:@selector(constantsToExport)] && ![target conformsToProtocol:@protocol(RCTNativeViewModule)]) { - // TODO: find a more elegant way to handle RCTNativeViewModule constants as a special case + if ([target respondsToSelector:@selector(constantsToExport)]) { moduleConfig[@"constants"] = [target constantsToExport]; } moduleConfigs[moduleName] = moduleConfig; @@ -484,6 +465,16 @@ static id _latestJSExecutor; } } +- (void)registerRootView:(RCTRootView *)rootView +{ + // TODO: only used by RCTUIManager - can we eliminate this special case? + for (id target in _moduleInstances.objectEnumerator) { + if ([target respondsToSelector:@selector(registerRootView:)]) { + [target registerRootView:rootView]; + } + } +} + + (BOOL)hasValidJSExecutor { return (_latestJSExecutor != nil && [_latestJSExecutor isValid]); diff --git a/ReactKit/Base/RCTExport.h b/ReactKit/Base/RCTExport.h index 11ca17693..f78370b9c 100644 --- a/ReactKit/Base/RCTExport.h +++ b/ReactKit/Base/RCTExport.h @@ -4,6 +4,7 @@ #import "RCTLog.h" +@class RCTBridge; @class RCTSparseArray; @class RCTUIManager; @@ -42,6 +43,12 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response); @protocol RCTNativeModule @optional +/** + * Optional initializer for modules that require access + * to bridge features, such as sending events or making JS calls + */ +- (instancetype)initWithBridge:(RCTBridge *)bridge; + /** * Place this macro inside the method body of any method you want * to expose to JS. The optional js_name argument will be used as @@ -74,7 +81,7 @@ _RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __ /** * Provides minimal interface needed to register a UIViewManager module */ -@protocol RCTNativeViewModule +@protocol RCTNativeViewModule /** * This method instantiates a native view to be managed by the module. @@ -83,6 +90,12 @@ _RCTExportSectionName))) static const RCTExportEntry __rct_export_entry__ = { __ @optional +/** + * The module name exposed to JS. If omitted, this will be inferred + * automatically by using the view module's class name. + */ ++ (NSString *)moduleName; + /** * This method instantiates a shadow view to be managed by the module. If omitted, * an ordinary RCTShadowView instance will be created. @@ -167,6 +180,12 @@ RCT_REMAP_VIEW_PROPERTY(name, name) */ - (NSDictionary *)customDirectEventTypes; +/** + * Injects constants into JS. These constants are made accessible via + * NativeModules.moduleName.X. + */ +- (NSDictionary *)constantsToExport; + /** * To deprecate, hopefully */ diff --git a/ReactKit/Base/RCTJavaScriptAppEngine.h b/ReactKit/Base/RCTJavaScriptAppEngine.h index 49136cd0a..0f1303424 100755 --- a/ReactKit/Base/RCTJavaScriptAppEngine.h +++ b/ReactKit/Base/RCTJavaScriptAppEngine.h @@ -14,9 +14,7 @@ * JavaScript so that applications can clean up resources. (launch blocker). * TODO: Incremental module loading. (low pri). */ -@interface RCTJavaScriptAppEngine : NSObject - -@property (nonatomic, readonly, strong) RCTBridge *bridge; +@interface RCTJavaScriptAppEngine : NSObject - (instancetype)initWithBridge:(RCTBridge *)bridge; - (void)loadBundleAtURL:(NSURL *)moduleURL useCache:(BOOL)useCache onComplete:(RCTJavaScriptCompleteBlock)onComplete; diff --git a/ReactKit/Base/RCTJavaScriptAppEngine.m b/ReactKit/Base/RCTJavaScriptAppEngine.m index 6ea5f4b28..bf1172457 100755 --- a/ReactKit/Base/RCTJavaScriptAppEngine.m +++ b/ReactKit/Base/RCTJavaScriptAppEngine.m @@ -22,11 +22,8 @@ */ @implementation RCTJavaScriptAppEngine { - BOOL _isPaused; // Pauses drawing/updating of the JSView - BOOL _pauseOnEnterBackground; - CADisplayLink *_displayLink; - NSTimer *_runTimer; NSDictionary *_loadedResource; + __weak RCTBridge *_bridge; } - (instancetype)init @@ -54,118 +51,13 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { RCTAssertMainThread(); + if ((self = [super init])) { _bridge = bridge; - _isPaused = NO; - self.pauseOnEnterBackground = YES; - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(run:)]; - if (_displayLink) { - [_displayLink setFrameInterval:1]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; - } else { - RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead."); - _runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(run:) userInfo:nil repeats:YES]; - } } return self; } -/** - * TODO: Wait until operations on `javaScriptQueue` are complete. - */ -- (void)dealloc -{ - RCTAssert(!self.valid, @"-invalidate must be called before -dealloc"); -} - -#pragma mark - RCTInvalidating - -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - [_bridge invalidate]; - _bridge = nil; - - [_displayLink invalidate]; - _displayLink = nil; - - // Remove from notification center - self.pauseOnEnterBackground = NO; -} - -#pragma mark - Run loop - -- (void)run:(CADisplayLink *)sender -{ - if (!_isPaused) { - RCTAssertMainThread(); - [_bridge enqueueUpdateTimers]; - } -} - -- (void)pauseRunLoop -{ - if (!_isPaused) { - [_displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; - _isPaused = YES; - } -} - -- (void)resumeRunLoop -{ - if (_isPaused) { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; - _isPaused = NO; - } -} - -/** - * See warnings from lint: UIApplicationDidBecomeActive fires in a critical - * foreground path, and prevents the app from prioritizing the foreground - * processing. Consider using - * FBApplicationDidFinishEnteringForegroundAndIsNowIdleNotification. - */ -- (void)setPauseOnEnterBackground:(BOOL)pauses -{ - NSArray *pauseN = @[ - UIApplicationWillResignActiveNotification, - UIApplicationDidEnterBackgroundNotification, - UIApplicationWillTerminateNotification - ]; - NSArray *resumeN = - @[UIApplicationWillEnterForegroundNotification, UIApplicationDidBecomeActiveNotification]; - - if (pauses) { - [self observeKeyPaths:pauseN selector:@selector(pauseRunLoop)]; - [self observeKeyPaths:resumeN selector:@selector(resumeRunLoop)]; - } - else { - [self removeObserverForKeyPaths:pauseN]; - [self removeObserverForKeyPaths:resumeN]; - } - _pauseOnEnterBackground = pauses; -} - -- (void)removeObserverForKeyPaths:(NSArray*)keyPaths -{ - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - for (NSString *name in keyPaths) { - [nc removeObserver:self name:name object:nil]; - } -} - -- (void)observeKeyPaths:(NSArray*)keyPaths selector:(SEL)selector -{ - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - for (NSString *name in keyPaths) { - [nc addObserver:self selector:selector name:name object:nil]; - } -} - #pragma mark - Module and script loading + (void)resetCacheForBundleAtURL:(NSURL *)moduleURL diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index 6f46b841d..a5e9b56f1 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -18,7 +18,6 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" @implementation RCTRootView { - dispatch_queue_t _shadowQueue; RCTBridge *_bridge; RCTJavaScriptAppEngine *_appEngine; RCTTouchHandler *_touchHandler; @@ -62,9 +61,6 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" - (void)setUp { - // TODO: does it make sense to do this here? What if there's more than one host view? - _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); - // Every root view that is created must have a unique react tag. // Numbering of these tags goes from 1, 11, 21, 31, etc static NSInteger rootViewTag = 1; @@ -95,16 +91,16 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" } } else { - [_bridge.uiManager registerRootView:self]; + [_bridge registerRootView:self]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": self.reactTag ?: @0, @"initialProps": self.initialProperties ?: @{}, }; - [_appEngine.bridge enqueueJSCall:RCTModuleIDBundler - methodID:RCTBundlerRunApplication - args:@[moduleName, appParameters]]; + [_bridge enqueueJSCall:RCTModuleIDBundler + methodID:RCTBundlerRunApplication + args:@[moduleName, appParameters]]; } } @@ -125,12 +121,10 @@ NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification" }; [_executor invalidate]; - [_appEngine invalidate]; [_bridge invalidate]; _executor = [[RCTContextExecutor alloc] init]; _bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor - shadowQueue:_shadowQueue javaScriptModulesConfig:[RCTModuleIDs config]]; _appEngine = [[RCTJavaScriptAppEngine alloc] initWithBridge:_bridge]; diff --git a/ReactKit/Modules/RCTTiming.h b/ReactKit/Modules/RCTTiming.h index 8cbcfe6bf..c7b7fcc35 100644 --- a/ReactKit/Modules/RCTTiming.h +++ b/ReactKit/Modules/RCTTiming.h @@ -3,12 +3,12 @@ #import #import "RCTExport.h" +#import "RCTInvalidating.h" @class RCTBridge; -@interface RCTTiming : NSObject +@interface RCTTiming : NSObject - (instancetype)initWithBridge:(RCTBridge *)bridge; -- (void)enqueueUpdateTimers; @end diff --git a/ReactKit/Modules/RCTTiming.m b/ReactKit/Modules/RCTTiming.m index e1a3f8cca..cc45fdbc2 100644 --- a/ReactKit/Modules/RCTTiming.m +++ b/ReactKit/Modules/RCTTiming.m @@ -11,7 +11,6 @@ @interface RCTTimer : NSObject @property (nonatomic, strong, readonly) NSDate *target; -@property (nonatomic, assign, readonly, getter=isActive) BOOL active; @property (nonatomic, assign, readonly) BOOL repeats; @property (nonatomic, strong, readonly) NSNumber *callbackID; @property (nonatomic, assign, readonly) NSTimeInterval interval; @@ -26,7 +25,6 @@ repeats:(BOOL)repeats { if ((self = [super init])) { - _active = YES; _interval = interval; _repeats = repeats; _callbackID = callbackID; @@ -40,13 +38,9 @@ */ - (BOOL)updateFoundNeedsJSUpdate { - if (_active && _target.timeIntervalSinceNow <= 0) { + if (_target && _target.timeIntervalSinceNow <= 0) { // The JS Timers will do fine grained calculating of expired timeouts. - if (_repeats) { - _target = [NSDate dateWithTimeIntervalSinceNow:_interval]; - } else { - _active = NO; - } + _target = _repeats ? [NSDate dateWithTimeIntervalSinceNow:_interval] : nil; return YES; } return NO; @@ -58,6 +52,7 @@ { RCTSparseArray *_timers; RCTBridge *_bridge; + id _updateTimer; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -65,19 +60,74 @@ if ((self = [super init])) { _bridge = bridge; _timers = [[RCTSparseArray alloc] init]; + [self startTimers]; + + for (NSString *name in @[UIApplicationWillResignActiveNotification, + UIApplicationDidEnterBackgroundNotification, + UIApplicationWillTerminateNotification]) { + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(stopTimers) + name:name + object:nil]; + } + + for (NSString *name in @[UIApplicationDidBecomeActiveNotification, + UIApplicationWillEnterForegroundNotification]) { + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startTimers) + name:name + object:nil]; + } } return self; } -/** - * TODO (#5906496): Wait until operations on `javaScriptQueue` are complete to complete the - * `dealloc`. - */ -/* - (void)dealloc - { - } */ +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} -- (void)enqueueUpdateTimers +- (BOOL)isValid +{ + return _bridge != nil; +} + +- (void)invalidate +{ + [self stopTimers]; + _bridge = nil; +} + +- (void)stopTimers +{ + [_updateTimer invalidate]; + _updateTimer = nil; +} + +- (void)startTimers +{ + RCTAssertMainThread(); + + if (![self isValid] || _updateTimer != nil) { + return; + } + + _updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; + if (_updateTimer) { + [_updateTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + } else { + RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead."); + _updateTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60) + target:self + selector:@selector(update) + userInfo:nil + repeats:YES]; + } +} + +- (void)update { RCTAssertMainThread(); @@ -86,7 +136,7 @@ if ([timer updateFoundNeedsJSUpdate]) { [timersToCall addObject:timer.callbackID]; } - if (!timer.active) { + if (!timer.target) { _timers[timer.callbackID] = nil; } } @@ -97,14 +147,6 @@ } } -- (void)scheduleCallbackID:(NSNumber *)callbackID interval:(NSTimeInterval)interval targetTime:(NSTimeInterval)targetTime repeats:(BOOL)repeats -{ - dispatch_async(dispatch_get_main_queue(), ^{ - RCTTimer *timer = [[RCTTimer alloc] initWithCallbackID:callbackID interval:interval targetTime:targetTime repeats:repeats]; - _timers[callbackID] = timer; - }); -} - /** * There's a small difference between the time when we call * setTimeout/setInterval/requestAnimation frame and the time it actually makes @@ -132,10 +174,13 @@ interval = 0; } - [self scheduleCallbackID:callbackID - interval:interval - targetTime:targetTime - repeats:repeats.boolValue]; + RCTTimer *timer = [[RCTTimer alloc] initWithCallbackID:callbackID + interval:interval + targetTime:targetTime + repeats:repeats]; + dispatch_async(dispatch_get_main_queue(), ^{ + _timers[callbackID] = timer; + }); } - (void)deleteTimer:(NSNumber *)timerID diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 32cc0f3c3..9547e096d 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -5,7 +5,7 @@ #import "RCTExport.h" #import "RCTInvalidating.h" -@class RCTAnimationRegistry; +@class RCTBridge; @class RCTRootView; @class RCTShadowView; @@ -14,13 +14,10 @@ @interface RCTUIManager : NSObject -- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue - viewManagers:(NSDictionary *)viewManagers; +- (instancetype)initWithBridge:(RCTBridge *)bridge; -@property (nonatomic, strong) RCTEventDispatcher *eventDispatcher; @property (nonatomic, strong) RCTSparseArray *shadowViewRegistry; @property (nonatomic, strong) RCTSparseArray *viewRegistry; -@property (nonatomic, strong) RCTAnimationRegistry *animationRegistry; @property (nonatomic, weak) id mainScrollView; /** diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 129572f4a..d12c8f998 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -34,6 +34,64 @@ static void RCTTraverseViewNodes(id view, react_view_node_b } } +static NSString *RCTModuleName(Class moduleClass) +{ + if ([moduleClass respondsToSelector:@selector(moduleName)]) { + + return [moduleClass moduleName]; + + } else { + + // Default implementation, works in most cases + NSString *className = NSStringFromClass(moduleClass); + if ([className hasPrefix:@"RCTUI"]) { + className = [className substringFromIndex:@"RCT".length]; + } + if ([className hasSuffix:@"Manager"]) { + className = [className substringToIndex:className.length - @"Manager".length]; + } + return className; + } +} + +static NSDictionary *RCTViewModuleClasses(void) +{ + static NSMutableDictionary *modules; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + modules = [NSMutableDictionary dictionary]; + + unsigned int classCount; + Class *classes = objc_copyClassList(&classCount); + for (unsigned int i = 0; i < classCount; i++) { + + Class cls = classes[i]; + + if (!class_getSuperclass(cls)) { + // Class has no superclass - it's probably something weird + continue; + } + + if (![cls conformsToProtocol:@protocol(RCTNativeViewModule)]) { + // Not an RCTNativeModule + continue; + } + + // Get module name + NSString *moduleName = RCTModuleName(cls); + + // Check module name is unique + id existingClass = modules[moduleName]; + RCTCAssert(existingClass == Nil, @"Attempted to register RCTNativeViewModule class %@ for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass); + modules[moduleName] = cls; + } + + free(classes); + }); + + return modules; +} + @implementation RCTUIManager { // Root views are only mutated on the shadow queue @@ -42,13 +100,14 @@ static void RCTTraverseViewNodes(id view, react_view_node_b NSMutableArray *_pendingUIBlocks; pthread_mutex_t _pendingUIBlocksMutex; - dispatch_queue_t _shadowQueue; NSDictionary *_nextLayoutAnimationConfig; // RCT thread only RCTResponseSenderBlock _nextLayoutAnimationCallback; // RCT thread only RCTResponseSenderBlock _layoutAnimationCallbackMT; // Main thread only NSMutableDictionary *_defaultShadowViews; NSMutableDictionary *_defaultViews; + + __weak RCTBridge *_bridge; } - (id )_managerInstanceForViewWithModuleName:(NSString *)moduleName @@ -62,17 +121,23 @@ static void RCTTraverseViewNodes(id view, react_view_node_b return managerInstance; } -- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue - viewManagers:(NSDictionary *)viewManagers +- (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { + + _bridge = bridge; + pthread_mutex_init(&_pendingUIBlocksMutex, NULL); + // Instantiate view managers + NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; + [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { + viewManagers[moduleName] = [[moduleClass alloc] init]; + }]; _viewManagers = viewManagers; + _viewRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init]; - _shadowQueue = shadowQueue; - pthread_mutex_init(&_pendingUIBlocksMutex, NULL); - + // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; @@ -101,6 +166,8 @@ static void RCTTraverseViewNodes(id view, react_view_node_b - (void)invalidate { + RCTAssertMainThread(); + _viewRegistry = nil; _shadowViewRegistry = nil; @@ -111,6 +178,8 @@ static void RCTTraverseViewNodes(id view, react_view_node_b - (void)registerRootView:(RCTRootView *)rootView; { + RCTAssertMainThread(); + NSNumber *reactTag = rootView.reactTag; UIView *existingView = _viewRegistry[reactTag]; RCTCAssert(existingView == nil || existingView == rootView, @@ -121,7 +190,7 @@ static void RCTTraverseViewNodes(id view, react_view_node_b CGRect frame = rootView.frame; // Register shadow view - dispatch_async(_shadowQueue, ^{ + dispatch_async(_bridge.shadowQueue, ^{ RCTShadowView *shadowView = [[RCTShadowView alloc] init]; shadowView.reactTag = reactTag; @@ -468,7 +537,7 @@ static void RCTTraverseViewNodes(id view, react_view_node_b - (UIView *)viewForViewManager:(id )manager { - return [manager viewWithEventDispatcher:_eventDispatcher]; + return [manager viewWithEventDispatcher:_bridge.eventDispatcher]; } static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, id manager) @@ -610,25 +679,25 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView // First copy the previous blocks into a temporary variable, then reset the // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. - + for (id viewManager in _viewManagers.allValues) { RCTViewManagerUIBlock uiBlock = [viewManager respondsToSelector:@selector(uiBlockToAmendWithShadowViewRegistry:)] ? [viewManager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry] : nil; if (uiBlock != nil) { [self addUIBlock:uiBlock]; } } - + for (NSNumber *reactTag in _rootViewTags) { RCTShadowView *rootView = _shadowViewRegistry[reactTag]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView]; } - + pthread_mutex_lock(&_pendingUIBlocksMutex); NSArray *previousPendingUIBlocks = _pendingUIBlocks; _pendingUIBlocks = [[NSMutableArray alloc] init]; pthread_mutex_unlock(&_pendingUIBlocksMutex); - + dispatch_async(dispatch_get_main_queue(), ^{ for (dispatch_block_t block in previousPendingUIBlocks) { block();