Skip to content

Commit 0e67e33

Browse files
committed
[ReactNative] Ensure JS calls scheduled by a deallocated context don't fire
1 parent 0b21df4 commit 0e67e33

File tree

6 files changed

+76
-35
lines changed

6 files changed

+76
-35
lines changed

Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ - (BOOL)prepareJSRuntime
8282
{
8383
__block NSError *initError;
8484
dispatch_semaphore_t s = dispatch_semaphore_create(0);
85-
[self sendMessage:@{@"method": @"prepareJSRuntime"} waitForReply:^(NSError *error, NSDictionary *reply) {
85+
[self sendMessage:@{@"method": @"prepareJSRuntime"} context:nil waitForReply:^(NSError *error, NSDictionary *reply) {
8686
initError = error;
8787
dispatch_semaphore_signal(s);
8888
}];
@@ -111,7 +111,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
111111
RCTLogError(@"WebSocket connection failed with error %@", error);
112112
}
113113

114-
- (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)callback
114+
- (void)sendMessage:(NSDictionary *)message context:(NSNumber *)executorID waitForReply:(WSMessageCallback)callback
115115
{
116116
static NSUInteger lastID = 10000;
117117

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

127129
NSNumber *expectedID = @(lastID++);
@@ -135,12 +137,12 @@ - (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)call
135137
- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
136138
{
137139
NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"url": [URL absoluteString], @"inject": _injectedObjects};
138-
[self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) {
140+
[self sendMessage:message context:nil waitForReply:^(NSError *error, NSDictionary *reply) {
139141
onComplete(error);
140142
}];
141143
}
142144

143-
- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete
145+
- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments context:(NSNumber *)executorID callback:(RCTJavaScriptCallback)onComplete
144146
{
145147
RCTAssert(onComplete != nil, @"callback was missing for exec JS call");
146148
NSDictionary *message = @{
@@ -149,7 +151,7 @@ - (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSAr
149151
@"moduleMethod": method,
150152
@"arguments": arguments
151153
};
152-
[self sendMessage:message waitForReply:^(NSError *socketError, NSDictionary *reply) {
154+
[self sendMessage:message context:executorID waitForReply:^(NSError *socketError, NSDictionary *reply) {
153155
if (socketError) {
154156
onComplete(nil, socketError);
155157
return;

React/Base/RCTBridge.m

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,13 @@ @interface RCTBridge ()
234234

235235
- (void)_invokeAndProcessModule:(NSString *)module
236236
method:(NSString *)method
237-
arguments:(NSArray *)args;
237+
arguments:(NSArray *)args
238+
context:(NSNumber *)context;
238239

239240
- (void)_actuallyInvokeAndProcessModule:(NSString *)module
240241
method:(NSString *)method
241-
arguments:(NSArray *)args;
242-
242+
arguments:(NSArray *)args
243+
context:(NSNumber *)context;
243244
@end
244245

245246
/**
@@ -338,7 +339,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
338339
NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
339340

340341
#define RCT_ARG_BLOCK(_logic) \
341-
[argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \
342+
[argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) { \
342343
_logic \
343344
[invocation setArgument:&value atIndex:index]; \
344345
}]; \
@@ -355,7 +356,8 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
355356
__autoreleasing id value = (json ? ^(NSArray *args) {
356357
[bridge _invokeAndProcessModule:@"BatchedBridge"
357358
method:@"invokeCallbackAndReturnFlushedQueue"
358-
arguments:@[json, args]];
359+
arguments:@[json, args]
360+
context:context];
359361
} : ^(NSArray *unused) {});
360362
)
361363
};
@@ -477,6 +479,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
477479
- (void)invokeWithBridge:(RCTBridge *)bridge
478480
module:(id)module
479481
arguments:(NSArray *)arguments
482+
context:(NSNumber *)context
480483
{
481484

482485
#if DEBUG
@@ -503,8 +506,8 @@ - (void)invokeWithBridge:(RCTBridge *)bridge
503506
NSUInteger index = 0;
504507
for (id json in arguments) {
505508
id arg = (json == [NSNull null]) ? nil : json;
506-
void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index];
507-
block(bridge, invocation, index + 2, arg);
509+
void (^block)(RCTBridge *, NSNumber *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index];
510+
block(bridge, context, invocation, index + 2, arg);
508511
index++;
509512
}
510513

@@ -653,7 +656,6 @@ - (NSString *)description
653656
return moduleConfig;
654657
}
655658

656-
657659
/**
658660
* As above, but for local modules/methods, which represent JS classes
659661
* and methods that will be called by the native code via the bridge.
@@ -801,7 +803,7 @@ @implementation RCTBridge
801803
RCTDisplayLink *_displayLink;
802804
NSMutableSet *_frameUpdateObservers;
803805
NSMutableArray *_scheduledCalls;
804-
NSMutableArray *_scheduledCallbacks;
806+
RCTSparseArray *_scheduledCallbacks;
805807
BOOL _loading;
806808

807809
NSUInteger _startingTime;
@@ -829,13 +831,13 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL
829831
- (void)setUp
830832
{
831833
Class executorClass = _executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class];
832-
_javaScriptExecutor = [[executorClass alloc] init];
834+
_javaScriptExecutor = RCTCreateExecutor(executorClass);
833835
_latestJSExecutor = _javaScriptExecutor;
834836
_eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
835837
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
836838
_frameUpdateObservers = [[NSMutableSet alloc] init];
837839
_scheduledCalls = [[NSMutableArray alloc] init];
838-
_scheduledCallbacks = [[NSMutableArray alloc] init];
840+
_scheduledCallbacks = [[RCTSparseArray alloc] init];
839841

840842
// Register passed-in module instances
841843
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
@@ -991,7 +993,6 @@ - (void)bindKeys
991993

992994
}
993995

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

@@ -1093,13 +1095,15 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer
10931095
#if BATCHED_BRIDGE
10941096
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
10951097
method:@"callFunctionReturnFlushedQueue"
1096-
arguments:@[moduleID, methodID, @[@[timer]]]];
1098+
arguments:@[moduleID, methodID, @[@[timer]]]
1099+
context:RCTGetExecutorID(_javaScriptExecutor)];
10971100

10981101
#else
10991102

11001103
[self _invokeAndProcessModule:@"BatchedBridge"
11011104
method:@"callFunctionReturnFlushedQueue"
1102-
arguments:@[moduleID, methodID, @[@[timer]]]];
1105+
arguments:@[moduleID, methodID, @[@[timer]]]
1106+
context:RCTGetExecutorID(_javaScriptExecutor)];
11031107
#endif
11041108
}
11051109
}
@@ -1108,6 +1112,7 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
11081112
{
11091113
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
11101114
RCT_PROFILE_START();
1115+
NSNumber *context = RCTGetExecutorID(_javaScriptExecutor);
11111116
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
11121117
RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script");
11131118
if (scriptLoadError) {
@@ -1119,10 +1124,11 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
11191124
[_javaScriptExecutor executeJSCall:@"BatchedBridge"
11201125
method:@"flushedQueue"
11211126
arguments:@[]
1127+
context:context
11221128
callback:^(id json, NSError *error) {
11231129
RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue");
11241130
RCT_PROFILE_START();
1125-
[self _handleBuffer:json];
1131+
[self _handleBuffer:json context:context];
11261132
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
11271133
onComplete(error);
11281134
}];
@@ -1131,7 +1137,7 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
11311137

11321138
#pragma mark - Payload Generation
11331139

1134-
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
1140+
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
11351141
{
11361142
#if BATCHED_BRIDGE
11371143
RCT_PROFILE_START();
@@ -1148,18 +1154,19 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg
11481154
@"module": module,
11491155
@"method": method,
11501156
@"args": args,
1157+
@"context": context ?: @0,
11511158
};
11521159

11531160
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
1154-
[_scheduledCallbacks addObject:call];
1161+
_scheduledCallbacks[args[0]] = call;
11551162
} else {
11561163
[_scheduledCalls addObject:call];
11571164
}
11581165

11591166
RCT_PROFILE_END(js_call, args, @"schedule", module, method);
11601167
}
11611168

1162-
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
1169+
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
11631170
{
11641171
#endif
11651172
[[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil];
@@ -1171,19 +1178,20 @@ - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)me
11711178
RCT_PROFILE_END(js_call, args, moduleDotMethod);
11721179

11731180
RCT_PROFILE_START();
1174-
[self _handleBuffer:json];
1181+
[self _handleBuffer:json context:context];
11751182
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
11761183
};
11771184

11781185
[_javaScriptExecutor executeJSCall:module
11791186
method:method
11801187
arguments:args
1188+
context:context
11811189
callback:processResponse];
11821190
}
11831191

11841192
#pragma mark - Payload Processing
11851193

1186-
- (void)_handleBuffer:(id)buffer
1194+
- (void)_handleBuffer:(id)buffer context:(NSNumber *)context
11871195
{
11881196
if (buffer == nil || buffer == (id)kCFNull) {
11891197
return;
@@ -1228,7 +1236,8 @@ - (void)_handleBuffer:(id)buffer
12281236
[self _handleRequestNumber:i
12291237
moduleID:[moduleIDs[i] integerValue]
12301238
methodID:[methodIDs[i] integerValue]
1231-
params:paramsArrays[i]];
1239+
params:paramsArrays[i]
1240+
context:context];
12321241
}
12331242
}
12341243

@@ -1247,6 +1256,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
12471256
moduleID:(NSUInteger)moduleID
12481257
methodID:(NSUInteger)methodID
12491258
params:(NSArray *)params
1259+
context:(NSNumber *)context
12501260
{
12511261
if (![params isKindOfClass:[NSArray class]]) {
12521262
RCTLogError(@"Invalid module/method/params tuple for request #%zd", i);
@@ -1280,7 +1290,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
12801290
}
12811291

12821292
@try {
1283-
[method invokeWithBridge:strongSelf module:module arguments:params];
1293+
[method invokeWithBridge:strongSelf module:module arguments:params context:context];
12841294
}
12851295
@catch (NSException *exception) {
12861296
RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception);
@@ -1313,13 +1323,18 @@ - (void)_runScheduledCalls
13131323
{
13141324
#if BATCHED_BRIDGE
13151325

1316-
NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls];
1326+
NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls];
1327+
NSNumber *currentExecutorID = RCTGetExecutorID(_javaScriptExecutor);
1328+
calls = [calls filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *call, NSDictionary *bindings) {
1329+
return [call[@"context"] isEqualToNumber:currentExecutorID];
1330+
}]];
13171331
if (calls.count > 0) {
13181332
_scheduledCalls = [[NSMutableArray alloc] init];
1319-
_scheduledCallbacks = [[NSMutableArray alloc] init];
1333+
_scheduledCallbacks = [[RCTSparseArray alloc] init];
13201334
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
1321-
method:@"processBatch"
1322-
arguments:@[calls]];
1335+
method:@"processBatch"
1336+
arguments:@[calls]
1337+
context:RCTGetExecutorID(_javaScriptExecutor)];
13231338
}
13241339

13251340
#endif
@@ -1357,6 +1372,7 @@ + (void)logMessage:(NSString *)message level:(NSString *)level
13571372
[_latestJSExecutor executeJSCall:@"RCTLog"
13581373
method:@"logIfNoNativeHook"
13591374
arguments:@[level, message]
1375+
context:RCTGetExecutorID(_latestJSExecutor)
13601376
callback:^(id json, NSError *error) {}];
13611377
}
13621378

React/Base/RCTJavaScriptExecutor.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*/
99

10+
#import <objc/runtime.h>
11+
1012
#import <JavaScriptCore/JavaScriptCore.h>
1113

1214
#import "RCTInvalidating.h"
@@ -27,6 +29,7 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
2729
- (void)executeJSCall:(NSString *)name
2830
method:(NSString *)method
2931
arguments:(NSArray *)arguments
32+
context:(NSNumber *)executorID
3033
callback:(RCTJavaScriptCallback)onComplete;
3134

3235
/**
@@ -40,3 +43,17 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
4043
asGlobalObjectNamed:(NSString *)objectName
4144
callback:(RCTJavaScriptCompleteBlock)onComplete;
4245
@end
46+
47+
static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID";
48+
__used static id<RCTJavaScriptExecutor> RCTCreateExecutor(Class executorClass)
49+
{
50+
static NSUInteger executorID = 0;
51+
id<RCTJavaScriptExecutor> executor = [[executorClass alloc] init];
52+
objc_setAssociatedObject(executor, RCTJavaScriptExecutorID, @(++executorID), OBJC_ASSOCIATION_RETAIN);
53+
return executor;
54+
}
55+
56+
__used static NSNumber *RCTGetExecutorID(id<RCTJavaScriptExecutor> executor)
57+
{
58+
return objc_getAssociatedObject(executor, RCTJavaScriptExecutorID);
59+
}

React/Base/RCTJavaScriptLoader.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onCom
140140
sourceCodeModule.scriptURL = scriptURL;
141141
sourceCodeModule.scriptText = rawText;
142142

143-
[_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *_error) {
143+
[_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
144144
dispatch_async(dispatch_get_main_queue(), ^{
145-
onComplete(_error);
145+
onComplete(scriptError);
146146
});
147147
}];
148148
}];

React/Executors/RCTContextExecutor.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,14 @@ - (void)dealloc
229229
- (void)executeJSCall:(NSString *)name
230230
method:(NSString *)method
231231
arguments:(NSArray *)arguments
232+
context:(NSNumber *)executorID
232233
callback:(RCTJavaScriptCallback)onComplete
233234
{
234235
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
235236
__weak RCTContextExecutor *weakSelf = self;
236237
[self executeBlockOnJavaScriptQueue:^{
237238
RCTContextExecutor *strongSelf = weakSelf;
238-
if (!strongSelf || !strongSelf.isValid) {
239+
if (!strongSelf || !strongSelf.isValid || ![RCTGetExecutorID(strongSelf) isEqualToNumber:executorID]) {
239240
return;
240241
}
241242
NSError *error;

React/Executors/RCTWebViewExecutor.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,15 @@ - (UIWebView *)invalidateAndReclaimWebView
7676
- (void)executeJSCall:(NSString *)name
7777
method:(NSString *)method
7878
arguments:(NSArray *)arguments
79+
context:(NSNumber *)executorID
7980
callback:(RCTJavaScriptCallback)onComplete
8081
{
8182
RCTAssert(onComplete != nil, @"");
8283
[self executeBlockOnJavaScriptQueue:^{
84+
if (!self.isValid || ![RCTGetExecutorID(self) isEqualToNumber:executorID]) {
85+
return;
86+
}
87+
8388
NSError *error;
8489
NSString *argsString = RCTJSONStringify(arguments, &error);
8590
if (!argsString) {

0 commit comments

Comments
 (0)