[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;
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;

View File

@ -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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _latestJSExecutor;
}
- (NSDictionary *)modules
{
RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. "
@ -1072,7 +1073,8 @@ static id<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _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<RCTJavaScriptExecutor> _latestJSExecutor;
[_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
arguments:@[level, message]
context:RCTGetExecutorID(_latestJSExecutor)
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.
*/
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
#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<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.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);
});
}];
}];

View File

@ -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;

View File

@ -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) {