Skip to content

Commit

Permalink
[ReactNative] Ensure JS calls scheduled by a deallocated context don'…
Browse files Browse the repository at this point in the history
…t fire
tadeuzagallo committed Apr 20, 2015

Unverified

This user has not yet uploaded their public signing key.
1 parent 0b21df4 commit 0e67e33
Showing 6 changed files with 76 additions and 35 deletions.
12 changes: 7 additions & 5 deletions Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ - (BOOL)prepareJSRuntime
{
__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 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)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;

@@ -122,6 +122,8 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)call
}];
callback(error, nil);
return;
} else if (executorID && ![RCTGetExecutorID(self) isEqualToNumber:executorID]) {
return;
}

NSNumber *expectedID = @(lastID++);
@@ -135,12 +137,12 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)call
- (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 @@ - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSAr
@"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;
70 changes: 43 additions & 27 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
@@ -234,12 +234,13 @@ @interface RCTBridge ()

- (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 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
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 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
__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 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
- (void)invokeWithBridge:(RCTBridge *)bridge
module:(id)module
arguments:(NSArray *)arguments
context:(NSNumber *)context
{

#if DEBUG
@@ -503,8 +506,8 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
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 @@ - (NSString *)description
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 @@ @implementation RCTBridge
RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers;
NSMutableArray *_scheduledCalls;
NSMutableArray *_scheduledCallbacks;
RCTSparseArray *_scheduledCallbacks;
BOOL _loading;

NSUInteger _startingTime;
@@ -829,13 +831,13 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL
- (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 @@ - (void)bindKeys

}


- (NSDictionary *)modules
{
RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. "
@@ -1072,7 +1073,8 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
if (!_loading) {
[self _invokeAndProcessModule:@"BatchedBridge"
method:@"callFunctionReturnFlushedQueue"
arguments:@[moduleID, methodID, args ?: @[]]];
arguments:@[moduleID, methodID, args ?: @[]]
context:RCTGetExecutorID(_javaScriptExecutor)];
}
}

@@ -1093,13 +1095,15 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer
#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 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
{
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 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
[_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 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:

#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,18 +1154,19 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg
@"module": module,
@"method": method,
@"args": args,
@"context": context ?: @0,
};

if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
[_scheduledCallbacks addObject:call];
_scheduledCallbacks[args[0]] = call;
} else {
[_scheduledCalls addObject:call];
}

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 @@ - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)me
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 @@ - (void)_handleBuffer:(id)buffer
[self _handleRequestNumber:i
moduleID:[moduleIDs[i] integerValue]
methodID:[methodIDs[i] integerValue]
params:paramsArrays[i]];
params:paramsArrays[i]
context:context];
}
}

@@ -1247,6 +1256,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
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 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
}

@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 @@ - (void)_runScheduledCalls
{
#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 @@ + (void)logMessage:(NSString *)message level:(NSString *)level
[_latestJSExecutor executeJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
arguments:@[level, message]
context:RCTGetExecutorID(_latestJSExecutor)
callback:^(id json, NSError *error) {}];
}

17 changes: 17 additions & 0 deletions React/Base/RCTJavaScriptExecutor.h
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 2 additions & 2 deletions React/Base/RCTJavaScriptLoader.m
Original file line number Diff line number Diff line change
@@ -140,9 +140,9 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onCom
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);
});
}];
}];
3 changes: 2 additions & 1 deletion React/Executors/RCTContextExecutor.m
Original file line number Diff line number Diff line change
@@ -229,13 +229,14 @@ - (void)dealloc
- (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;
5 changes: 5 additions & 0 deletions React/Executors/RCTWebViewExecutor.m
Original file line number Diff line number Diff line change
@@ -76,10 +76,15 @@ - (UIWebView *)invalidateAndReclaimWebView
- (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) {

0 comments on commit 0e67e33

Please sign in to comment.