[ReactNative] Ensure JS calls scheduled by a deallocated context don't fire

This commit is contained in:
Tadeu Zagallo
2015-04-20 02:09:11 -07:00
parent 0b21df4a34
commit 0e67e33534
6 changed files with 76 additions and 35 deletions

View File

@@ -82,7 +82,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
{ {
__block NSError *initError; __block NSError *initError;
dispatch_semaphore_t s = dispatch_semaphore_create(0); 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; initError = error;
dispatch_semaphore_signal(s); dispatch_semaphore_signal(s);
}]; }];
@@ -111,7 +111,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
RCTLogError(@"WebSocket connection failed with error %@", error); 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; static NSUInteger lastID = 10000;
@@ -122,6 +122,8 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
}]; }];
callback(error, nil); callback(error, nil);
return; return;
} else if (executorID && ![RCTGetExecutorID(self) isEqualToNumber:executorID]) {
return;
} }
NSNumber *expectedID = @(lastID++); NSNumber *expectedID = @(lastID++);
@@ -135,12 +137,12 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete - (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
{ {
NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"url": [URL absoluteString], @"inject": _injectedObjects}; 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); 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"); RCTAssert(onComplete != nil, @"callback was missing for exec JS call");
NSDictionary *message = @{ NSDictionary *message = @{
@@ -149,7 +151,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
@"moduleMethod": method, @"moduleMethod": method,
@"arguments": arguments @"arguments": arguments
}; };
[self sendMessage:message waitForReply:^(NSError *socketError, NSDictionary *reply) { [self sendMessage:message context:executorID waitForReply:^(NSError *socketError, NSDictionary *reply) {
if (socketError) { if (socketError) {
onComplete(nil, socketError); onComplete(nil, socketError);
return; return;

View File

@@ -234,12 +234,13 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
- (void)_invokeAndProcessModule:(NSString *)module - (void)_invokeAndProcessModule:(NSString *)module
method:(NSString *)method method:(NSString *)method
arguments:(NSArray *)args; arguments:(NSArray *)args
context:(NSNumber *)context;
- (void)_actuallyInvokeAndProcessModule:(NSString *)module - (void)_actuallyInvokeAndProcessModule:(NSString *)module
method:(NSString *)method method:(NSString *)method
arguments:(NSArray *)args; arguments:(NSArray *)args
context:(NSNumber *)context;
@end @end
/** /**
@@ -338,7 +339,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName)
NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
#define RCT_ARG_BLOCK(_logic) \ #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 \ _logic \
[invocation setArgument:&value atIndex:index]; \ [invocation setArgument:&value atIndex:index]; \
}]; \ }]; \
@@ -355,7 +356,8 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName)
__autoreleasing id value = (json ? ^(NSArray *args) { __autoreleasing id value = (json ? ^(NSArray *args) {
[bridge _invokeAndProcessModule:@"BatchedBridge" [bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue" method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, args]]; arguments:@[json, args]
context:context];
} : ^(NSArray *unused) {}); } : ^(NSArray *unused) {});
) )
}; };
@@ -477,6 +479,7 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName)
- (void)invokeWithBridge:(RCTBridge *)bridge - (void)invokeWithBridge:(RCTBridge *)bridge
module:(id)module module:(id)module
arguments:(NSArray *)arguments arguments:(NSArray *)arguments
context:(NSNumber *)context
{ {
#if DEBUG #if DEBUG
@@ -503,8 +506,8 @@ static NSString *RCTStringUpToFirstArgument(NSString *methodName)
NSUInteger index = 0; NSUInteger index = 0;
for (id json in arguments) { for (id json in arguments) {
id arg = (json == [NSNull null]) ? nil : json; id arg = (json == [NSNull null]) ? nil : json;
void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index];
block(bridge, invocation, index + 2, arg); block(bridge, context, invocation, index + 2, arg);
index++; index++;
} }
@@ -653,7 +656,6 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName)
return moduleConfig; return moduleConfig;
} }
/** /**
* As above, but for local modules/methods, which represent JS classes * As above, but for local modules/methods, which represent JS classes
* and methods that will be called by the native code via the bridge. * and methods that will be called by the native code via the bridge.
@@ -801,7 +803,7 @@ static NSDictionary *RCTLocalModulesConfig()
RCTDisplayLink *_displayLink; RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers; NSMutableSet *_frameUpdateObservers;
NSMutableArray *_scheduledCalls; NSMutableArray *_scheduledCalls;
NSMutableArray *_scheduledCallbacks; RCTSparseArray *_scheduledCallbacks;
BOOL _loading; BOOL _loading;
NSUInteger _startingTime; NSUInteger _startingTime;
@@ -829,13 +831,13 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
- (void)setUp - (void)setUp
{ {
Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class]; Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class];
_javaScriptExecutor = [[executorClass alloc] init]; _javaScriptExecutor = RCTCreateExecutor(executorClass);
_latestJSExecutor = _javaScriptExecutor; _latestJSExecutor = _javaScriptExecutor;
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; _displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
_frameUpdateObservers = [[NSMutableSet alloc] init]; _frameUpdateObservers = [[NSMutableSet alloc] init];
_scheduledCalls = [[NSMutableArray alloc] init]; _scheduledCalls = [[NSMutableArray alloc] init];
_scheduledCallbacks = [[NSMutableArray alloc] init]; _scheduledCallbacks = [[RCTSparseArray alloc] init];
// Register passed-in module instances // Register passed-in module instances
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
@@ -991,7 +993,6 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
} }
- (NSDictionary *)modules - (NSDictionary *)modules
{ {
RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. "
@@ -1072,7 +1073,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
if (!_loading) { if (!_loading) {
[self _invokeAndProcessModule:@"BatchedBridge" [self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue" method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, args ?: @[]]]; arguments:@[moduleID, methodID, args ?: @[]]
context:RCTGetExecutorID(_javaScriptExecutor)];
} }
} }
@@ -1093,13 +1095,15 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
#if BATCHED_BRIDGE #if BATCHED_BRIDGE
[self _actuallyInvokeAndProcessModule:@"BatchedBridge" [self _actuallyInvokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue" method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, @[@[timer]]]]; arguments:@[moduleID, methodID, @[@[timer]]]
context:RCTGetExecutorID(_javaScriptExecutor)];
#else #else
[self _invokeAndProcessModule:@"BatchedBridge" [self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue" method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, @[@[timer]]]]; arguments:@[moduleID, methodID, @[@[timer]]]
context:RCTGetExecutorID(_javaScriptExecutor)];
#endif #endif
} }
} }
@@ -1108,6 +1112,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
{ {
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
RCT_PROFILE_START(); RCT_PROFILE_START();
NSNumber *context = RCTGetExecutorID(_javaScriptExecutor);
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script"); RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script");
if (scriptLoadError) { if (scriptLoadError) {
@@ -1119,10 +1124,11 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
[_javaScriptExecutor executeJSCall:@"BatchedBridge" [_javaScriptExecutor executeJSCall:@"BatchedBridge"
method:@"flushedQueue" method:@"flushedQueue"
arguments:@[] arguments:@[]
context:context
callback:^(id json, NSError *error) { callback:^(id json, NSError *error) {
RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue"); RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue");
RCT_PROFILE_START(); RCT_PROFILE_START();
[self _handleBuffer:json]; [self _handleBuffer:json context:context];
RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
onComplete(error); onComplete(error);
}]; }];
@@ -1131,7 +1137,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
#pragma mark - Payload Generation #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 #if BATCHED_BRIDGE
RCT_PROFILE_START(); RCT_PROFILE_START();
@@ -1148,10 +1154,11 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
@"module": module, @"module": module,
@"method": method, @"method": method,
@"args": args, @"args": args,
@"context": context ?: @0,
}; };
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
[_scheduledCallbacks addObject:call]; _scheduledCallbacks[args[0]] = call;
} else { } else {
[_scheduledCalls addObject:call]; [_scheduledCalls addObject:call];
} }
@@ -1159,7 +1166,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
RCT_PROFILE_END(js_call, args, @"schedule", module, method); 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 #endif
[[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil];
@@ -1171,19 +1178,20 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
RCT_PROFILE_END(js_call, args, moduleDotMethod); RCT_PROFILE_END(js_call, args, moduleDotMethod);
RCT_PROFILE_START(); RCT_PROFILE_START();
[self _handleBuffer:json]; [self _handleBuffer:json context:context];
RCT_PROFILE_END(objc_call, json, @"batched_js_calls"); RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
}; };
[_javaScriptExecutor executeJSCall:module [_javaScriptExecutor executeJSCall:module
method:method method:method
arguments:args arguments:args
context:context
callback:processResponse]; callback:processResponse];
} }
#pragma mark - Payload Processing #pragma mark - Payload Processing
- (void)_handleBuffer:(id)buffer - (void)_handleBuffer:(id)buffer context:(NSNumber *)context
{ {
if (buffer == nil || buffer == (id)kCFNull) { if (buffer == nil || buffer == (id)kCFNull) {
return; return;
@@ -1228,7 +1236,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
[self _handleRequestNumber:i [self _handleRequestNumber:i
moduleID:[moduleIDs[i] integerValue] moduleID:[moduleIDs[i] integerValue]
methodID:[methodIDs[i] integerValue] methodID:[methodIDs[i] integerValue]
params:paramsArrays[i]]; params:paramsArrays[i]
context:context];
} }
} }
@@ -1247,6 +1256,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
moduleID:(NSUInteger)moduleID moduleID:(NSUInteger)moduleID
methodID:(NSUInteger)methodID methodID:(NSUInteger)methodID
params:(NSArray *)params params:(NSArray *)params
context:(NSNumber *)context
{ {
if (![params isKindOfClass:[NSArray class]]) { if (![params isKindOfClass:[NSArray class]]) {
RCTLogError(@"Invalid module/method/params tuple for request #%zd", i); RCTLogError(@"Invalid module/method/params tuple for request #%zd", i);
@@ -1280,7 +1290,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
} }
@try { @try {
[method invokeWithBridge:strongSelf module:module arguments:params]; [method invokeWithBridge:strongSelf module:module arguments:params context:context];
} }
@catch (NSException *exception) { @catch (NSException *exception) {
RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception);
@@ -1313,13 +1323,18 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
{ {
#if BATCHED_BRIDGE #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) { if (calls.count > 0) {
_scheduledCalls = [[NSMutableArray alloc] init]; _scheduledCalls = [[NSMutableArray alloc] init];
_scheduledCallbacks = [[NSMutableArray alloc] init]; _scheduledCallbacks = [[RCTSparseArray alloc] init];
[self _actuallyInvokeAndProcessModule:@"BatchedBridge" [self _actuallyInvokeAndProcessModule:@"BatchedBridge"
method:@"processBatch" method:@"processBatch"
arguments:@[calls]]; arguments:@[calls]
context:RCTGetExecutorID(_javaScriptExecutor)];
} }
#endif #endif
@@ -1357,6 +1372,7 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
[_latestJSExecutor executeJSCall:@"RCTLog" [_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook" method:@"logIfNoNativeHook"
arguments:@[level, message] arguments:@[level, message]
context:RCTGetExecutorID(_latestJSExecutor)
callback:^(id json, NSError *error) {}]; callback:^(id json, NSError *error) {}];
} }

View File

@@ -7,6 +7,8 @@
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
*/ */
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h> #import <JavaScriptCore/JavaScriptCore.h>
#import "RCTInvalidating.h" #import "RCTInvalidating.h"
@@ -27,6 +29,7 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
- (void)executeJSCall:(NSString *)name - (void)executeJSCall:(NSString *)name
method:(NSString *)method method:(NSString *)method
arguments:(NSArray *)arguments arguments:(NSArray *)arguments
context:(NSNumber *)executorID
callback:(RCTJavaScriptCallback)onComplete; callback:(RCTJavaScriptCallback)onComplete;
/** /**
@@ -40,3 +43,17 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
asGlobalObjectNamed:(NSString *)objectName asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete; callback:(RCTJavaScriptCompleteBlock)onComplete;
@end @end
static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID";
__used static id<RCTJavaScriptExecutor> RCTCreateExecutor(Class executorClass)
{
static NSUInteger executorID = 0;
id<RCTJavaScriptExecutor> executor = [[executorClass alloc] init];
objc_setAssociatedObject(executor, RCTJavaScriptExecutorID, @(++executorID), OBJC_ASSOCIATION_RETAIN);
return executor;
}
__used static NSNumber *RCTGetExecutorID(id<RCTJavaScriptExecutor> executor)
{
return objc_getAssociatedObject(executor, RCTJavaScriptExecutorID);
}

View File

@@ -140,9 +140,9 @@
sourceCodeModule.scriptURL = scriptURL; sourceCodeModule.scriptURL = scriptURL;
sourceCodeModule.scriptText = rawText; 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(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
onComplete(_error); onComplete(scriptError);
}); });
}]; }];
}]; }];

View File

@@ -229,13 +229,14 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
- (void)executeJSCall:(NSString *)name - (void)executeJSCall:(NSString *)name
method:(NSString *)method method:(NSString *)method
arguments:(NSArray *)arguments arguments:(NSArray *)arguments
context:(NSNumber *)executorID
callback:(RCTJavaScriptCallback)onComplete callback:(RCTJavaScriptCallback)onComplete
{ {
RCTAssert(onComplete != nil, @"onComplete block should not be nil"); RCTAssert(onComplete != nil, @"onComplete block should not be nil");
__weak RCTContextExecutor *weakSelf = self; __weak RCTContextExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:^{ [self executeBlockOnJavaScriptQueue:^{
RCTContextExecutor *strongSelf = weakSelf; RCTContextExecutor *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.isValid) { if (!strongSelf || !strongSelf.isValid || ![RCTGetExecutorID(strongSelf) isEqualToNumber:executorID]) {
return; return;
} }
NSError *error; NSError *error;

View File

@@ -76,10 +76,15 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...)
- (void)executeJSCall:(NSString *)name - (void)executeJSCall:(NSString *)name
method:(NSString *)method method:(NSString *)method
arguments:(NSArray *)arguments arguments:(NSArray *)arguments
context:(NSNumber *)executorID
callback:(RCTJavaScriptCallback)onComplete callback:(RCTJavaScriptCallback)onComplete
{ {
RCTAssert(onComplete != nil, @""); RCTAssert(onComplete != nil, @"");
[self executeBlockOnJavaScriptQueue:^{ [self executeBlockOnJavaScriptQueue:^{
if (!self.isValid || ![RCTGetExecutorID(self) isEqualToNumber:executorID]) {
return;
}
NSError *error; NSError *error;
NSString *argsString = RCTJSONStringify(arguments, &error); NSString *argsString = RCTJSONStringify(arguments, &error);
if (!argsString) { if (!argsString) {