From 0e67e33534e8f2e7652ad83f4395a64d955517d4 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Mon, 20 Apr 2015 02:09:11 -0700 Subject: [PATCH] [ReactNative] Ensure JS calls scheduled by a deallocated context don't fire --- .../RCTWebSocketExecutor.m | 12 ++-- React/Base/RCTBridge.m | 70 ++++++++++++------- React/Base/RCTJavaScriptExecutor.h | 17 +++++ React/Base/RCTJavaScriptLoader.m | 4 +- React/Executors/RCTContextExecutor.m | 3 +- React/Executors/RCTWebViewExecutor.m | 5 ++ 6 files changed, 76 insertions(+), 35 deletions(-) diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index 784c91e12..7fd817d53 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -82,7 +82,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); { __block NSError *initError; dispatch_semaphore_t s = dispatch_semaphore_create(0); - [self sendMessage:@{@"method": @"prepareJSRuntime"} waitForReply:^(NSError *error, NSDictionary *reply) { + [self sendMessage:@{@"method": @"prepareJSRuntime"} context:nil waitForReply:^(NSError *error, NSDictionary *reply) { initError = error; dispatch_semaphore_signal(s); }]; @@ -111,7 +111,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); RCTLogError(@"WebSocket connection failed with error %@", error); } -- (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)callback +- (void)sendMessage:(NSDictionary *)message context:(NSNumber *)executorID waitForReply:(WSMessageCallback)callback { static NSUInteger lastID = 10000; @@ -122,6 +122,8 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); }]; callback(error, nil); return; + } else if (executorID && ![RCTGetExecutorID(self) isEqualToNumber:executorID]) { + return; } NSNumber *expectedID = @(lastID++); @@ -135,12 +137,12 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); - (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete { NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"url": [URL absoluteString], @"inject": _injectedObjects}; - [self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) { + [self sendMessage:message context:nil waitForReply:^(NSError *error, NSDictionary *reply) { onComplete(error); }]; } -- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete +- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @"callback was missing for exec JS call"); NSDictionary *message = @{ @@ -149,7 +151,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); @"moduleMethod": method, @"arguments": arguments }; - [self sendMessage:message waitForReply:^(NSError *socketError, NSDictionary *reply) { + [self sendMessage:message context:executorID waitForReply:^(NSError *socketError, NSDictionary *reply) { if (socketError) { onComplete(nil, socketError); return; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 7b1f95b69..a2ff0146b 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -234,12 +234,13 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method - arguments:(NSArray *)args; + arguments:(NSArray *)args + context:(NSNumber *)context; - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method - arguments:(NSArray *)args; - + arguments:(NSArray *)args + context:(NSNumber *)context; @end /** @@ -338,7 +339,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; #define RCT_ARG_BLOCK(_logic) \ - [argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \ + [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { \ _logic \ [invocation setArgument:&value atIndex:index]; \ }]; \ @@ -355,7 +356,8 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) __autoreleasing id value = (json ? ^(NSArray *args) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, args]]; + arguments:@[json, args] + context:context]; } : ^(NSArray *unused) {}); ) }; @@ -477,6 +479,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) - (void)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments + context:(NSNumber *)context { #if DEBUG @@ -503,8 +506,8 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName) NSUInteger index = 0; for (id json in arguments) { id arg = (json == [NSNull null]) ? nil : json; - void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; - block(bridge, invocation, index + 2, arg); + void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; + block(bridge, context, invocation, index + 2, arg); index++; } @@ -653,7 +656,6 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) return moduleConfig; } - /** * As above, but for local modules/methods, which represent JS classes * and methods that will be called by the native code via the bridge. @@ -801,7 +803,7 @@ static NSDictionary *RCTLocalModulesConfig() RCTDisplayLink *_displayLink; NSMutableSet *_frameUpdateObservers; NSMutableArray *_scheduledCalls; - NSMutableArray *_scheduledCallbacks; + RCTSparseArray *_scheduledCallbacks; BOOL _loading; NSUInteger _startingTime; @@ -829,13 +831,13 @@ static id _latestJSExecutor; - (void)setUp { Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; - _javaScriptExecutor = [[executorClass alloc] init]; + _javaScriptExecutor = RCTCreateExecutor(executorClass); _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; _frameUpdateObservers = [[NSMutableSet alloc] init]; _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; @@ -991,7 +993,6 @@ static id _latestJSExecutor; } - - (NSDictionary *)modules { RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " @@ -1072,7 +1073,8 @@ static id _latestJSExecutor; if (!_loading) { [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, args ?: @[]]]; + arguments:@[moduleID, methodID, args ?: @[]] + context:RCTGetExecutorID(_javaScriptExecutor)]; } } @@ -1093,13 +1095,15 @@ static id _latestJSExecutor; #if BATCHED_BRIDGE [self _actuallyInvokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]]]; + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; #else [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]]]; + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; #endif } } @@ -1108,6 +1112,7 @@ static id _latestJSExecutor; { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); RCT_PROFILE_START(); + NSNumber *context = RCTGetExecutorID(_javaScriptExecutor); [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script"); if (scriptLoadError) { @@ -1119,10 +1124,11 @@ static id _latestJSExecutor; [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] + context:context callback:^(id json, NSError *error) { RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue"); RCT_PROFILE_START(); - [self _handleBuffer:json]; + [self _handleBuffer:json context:context]; RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); onComplete(error); }]; @@ -1131,7 +1137,7 @@ static id _latestJSExecutor; #pragma mark - Payload Generation -- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { #if BATCHED_BRIDGE RCT_PROFILE_START(); @@ -1148,10 +1154,11 @@ static id _latestJSExecutor; @"module": module, @"method": method, @"args": args, + @"context": context ?: @0, }; if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - [_scheduledCallbacks addObject:call]; + _scheduledCallbacks[args[0]] = call; } else { [_scheduledCalls addObject:call]; } @@ -1159,7 +1166,7 @@ static id _latestJSExecutor; RCT_PROFILE_END(js_call, args, @"schedule", module, method); } -- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { #endif [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; @@ -1171,19 +1178,20 @@ static id _latestJSExecutor; RCT_PROFILE_END(js_call, args, moduleDotMethod); RCT_PROFILE_START(); - [self _handleBuffer:json]; + [self _handleBuffer:json context:context]; RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); }; [_javaScriptExecutor executeJSCall:module method:method arguments:args + context:context callback:processResponse]; } #pragma mark - Payload Processing -- (void)_handleBuffer:(id)buffer +- (void)_handleBuffer:(id)buffer context:(NSNumber *)context { if (buffer == nil || buffer == (id)kCFNull) { return; @@ -1228,7 +1236,8 @@ static id _latestJSExecutor; [self _handleRequestNumber:i moduleID:[moduleIDs[i] integerValue] methodID:[methodIDs[i] integerValue] - params:paramsArrays[i]]; + params:paramsArrays[i] + context:context]; } } @@ -1247,6 +1256,7 @@ static id _latestJSExecutor; moduleID:(NSUInteger)moduleID methodID:(NSUInteger)methodID params:(NSArray *)params + context:(NSNumber *)context { if (![params isKindOfClass:[NSArray class]]) { RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); @@ -1280,7 +1290,7 @@ static id _latestJSExecutor; } @try { - [method invokeWithBridge:strongSelf module:module arguments:params]; + [method invokeWithBridge:strongSelf module:module arguments:params context:context]; } @catch (NSException *exception) { RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); @@ -1313,13 +1323,18 @@ static id _latestJSExecutor; { #if BATCHED_BRIDGE - NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls]; + NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; + NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor); + calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) { + return [call[@"context"] isEqualToNumber:currentExecutorID]; + }]]; if (calls.count > 0) { _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[NSMutableArray alloc] init]; + _scheduledCallbacks = [[RCTSparseArray alloc] init]; [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"processBatch" - arguments:@[calls]]; + method:@"processBatch" + arguments:@[calls] + context:RCTGetExecutorID(_javaScriptExecutor)]; } #endif @@ -1357,6 +1372,7 @@ static id _latestJSExecutor; [_latestJSExecutor executeJSCall:@"RCTLog" method:@"logIfNoNativeHook" arguments:@[level, message] + context:RCTGetExecutorID(_latestJSExecutor) callback:^(id json, NSError *error) {}]; } diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index 57dff78e7..2816c7a7a 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import #import "RCTInvalidating.h" @@ -27,6 +29,7 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete; /** @@ -40,3 +43,17 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete; @end + +static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID"; +__used static id RCTCreateExecutor(Class executorClass) +{ + static NSUInteger executorID = 0; + id executor = [[executorClass alloc] init]; + objc_setAssociatedObject(executor, RCTJavaScriptExecutorID, @(++executorID), OBJC_ASSOCIATION_RETAIN); + return executor; +} + +__used static NSNumber *RCTGetExecutorID(id executor) +{ + return objc_getAssociatedObject(executor, RCTJavaScriptExecutorID); +} diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 02785fb41..4eaaf278d 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -140,9 +140,9 @@ sourceCodeModule.scriptURL = scriptURL; sourceCodeModule.scriptText = rawText; - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *_error) { + [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(_error); + onComplete(scriptError); }); }]; }]; diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 39616bf35..71e4f45c3 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -229,13 +229,14 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @"onComplete block should not be nil"); __weak RCTContextExecutor *weakSelf = self; [self executeBlockOnJavaScriptQueue:^{ RCTContextExecutor *strongSelf = weakSelf; - if (!strongSelf || !strongSelf.isValid) { + if (!strongSelf || !strongSelf.isValid || ![RCTGetExecutorID(strongSelf) isEqualToNumber:executorID]) { return; } NSError *error; diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index 55de44ab9..0bc6fdfc8 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -76,10 +76,15 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments + context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @""); [self executeBlockOnJavaScriptQueue:^{ + if (!self.isValid || ![RCTGetExecutorID(self) isEqualToNumber:executorID]) { + return; + } + NSError *error; NSString *argsString = RCTJSONStringify(arguments, &error); if (!argsString) {