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
  • Loading branch information
tadeuzagallo committed Apr 20, 2015
1 parent 0b21df4 commit 0e67e33
Show file tree
Hide file tree
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
Expand Up @@ -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);
}];
Expand Down Expand Up @@ -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;

Expand All @@ -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++);
Expand All @@ -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 = @{
Expand All @@ -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;
Expand Down
70 changes: 43 additions & 27 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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]; \
}]; \
Expand All @@ -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) {});
)
};
Expand Down Expand Up @@ -477,6 +479,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
- (void)invokeWithBridge:(RCTBridge *)bridge
module:(id)module
arguments:(NSArray *)arguments
context:(NSNumber *)context
{

#if DEBUG
Expand All @@ -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++;
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -801,7 +803,7 @@ @implementation RCTBridge
RCTDisplayLink *_displayLink;
NSMutableSet *_frameUpdateObservers;
NSMutableArray *_scheduledCalls;
NSMutableArray *_scheduledCallbacks;
RCTSparseArray *_scheduledCallbacks;
BOOL _loading;

NSUInteger _startingTime;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -991,7 +993,6 @@ - (void)bindKeys

}


- (NSDictionary *)modules
{
RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. "
Expand Down Expand Up @@ -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)];
}
}

Expand All @@ -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
}
}
Expand All @@ -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) {
Expand All @@ -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);
}];
Expand All @@ -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();
Expand All @@ -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];
Expand All @@ -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;
Expand Down Expand Up @@ -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];
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {}];
}

Expand Down
17 changes: 17 additions & 0 deletions React/Base/RCTJavaScriptExecutor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;

/**
Expand All @@ -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
Expand Up @@ -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);
});
}];
}];
Expand Down
3 changes: 2 additions & 1 deletion React/Executors/RCTContextExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions React/Executors/RCTWebViewExecutor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 0e67e33

Please sign in to comment.