Skip to content

Commit 2f4430c

Browse files
committed
[ReactNative] Fix bridge event dedupe
1 parent f7276b0 commit 2f4430c

File tree

3 files changed

+113
-35
lines changed

3 files changed

+113
-35
lines changed

React/Base/RCTBridge.m

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ - (NSString *)description
672672
*/
673673
static NSMutableDictionary *RCTLocalModuleIDs;
674674
static NSMutableDictionary *RCTLocalMethodIDs;
675+
static NSMutableArray *RCTLocalModuleNames;
676+
static NSMutableArray *RCTLocalMethodNames;
675677
static NSDictionary *RCTLocalModulesConfig()
676678
{
677679
static NSMutableDictionary *localModules;
@@ -680,6 +682,8 @@ - (NSString *)description
680682

681683
RCTLocalModuleIDs = [[NSMutableDictionary alloc] init];
682684
RCTLocalMethodIDs = [[NSMutableDictionary alloc] init];
685+
RCTLocalModuleNames = [[NSMutableArray alloc] init];
686+
RCTLocalMethodNames = [[NSMutableArray alloc] init];
683687

684688
localModules = [[NSMutableDictionary alloc] init];
685689
for (NSString *moduleDotMethod in RCTJSMethods()) {
@@ -711,6 +715,8 @@ - (NSString *)description
711715
// Add module and method lookup
712716
RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"];
713717
RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"];
718+
[RCTLocalModuleNames addObject:moduleName];
719+
[RCTLocalMethodNames addObject:methodName];
714720
}
715721
});
716722

@@ -1072,7 +1078,7 @@ - (void)invalidate
10721078
#pragma mark - RCTBridge methods
10731079

10741080
/**
1075-
* Like JS::call, for objective-c.
1081+
* Public. Can be invoked from any thread.
10761082
*/
10771083
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
10781084
{
@@ -1083,12 +1089,10 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
10831089
NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod];
10841090
RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod);
10851091

1086-
if (!_loading) {
1087-
[self _invokeAndProcessModule:@"BatchedBridge"
1088-
method:@"callFunctionReturnFlushedQueue"
1089-
arguments:@[moduleID, methodID, args ?: @[]]
1090-
context:RCTGetExecutorID(_javaScriptExecutor)];
1091-
}
1092+
[self _invokeAndProcessModule:@"BatchedBridge"
1093+
method:@"callFunctionReturnFlushedQueue"
1094+
arguments:@[moduleID, methodID, args ?: @[]]
1095+
context:RCTGetExecutorID(_javaScriptExecutor)];
10921096
}
10931097

10941098
/**
@@ -1106,10 +1110,17 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer
11061110

11071111
if (!_loading) {
11081112
#if BATCHED_BRIDGE
1109-
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
1110-
method:@"callFunctionReturnFlushedQueue"
1111-
arguments:@[moduleID, methodID, @[@[timer]]]
1112-
context:RCTGetExecutorID(_javaScriptExecutor)];
1113+
dispatch_block_t block = ^{
1114+
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
1115+
method:@"callFunctionReturnFlushedQueue"
1116+
arguments:@[moduleID, methodID, @[@[timer]]]
1117+
context:RCTGetExecutorID(_javaScriptExecutor)];
1118+
};
1119+
if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) {
1120+
[_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block];
1121+
} else {
1122+
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
1123+
}
11131124

11141125
#else
11151126

@@ -1163,33 +1174,83 @@ - (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID
11631174
}
11641175
}
11651176

1177+
/**
1178+
* Called by enqueueJSCall from any thread, or from _immediatelyCallTimer,
1179+
* on the JS thread, but only in non-batched mode.
1180+
*/
11661181
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
11671182
{
11681183
#if BATCHED_BRIDGE
1169-
RCTProfileBeginEvent();
11701184

1171-
if ([module isEqualToString:@"RCTEventEmitter"]) {
1172-
for (NSDictionary *call in _scheduledCalls) {
1173-
if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) {
1174-
[_scheduledCalls removeObject:call];
1185+
__weak NSMutableArray *weakScheduledCalls = _scheduledCalls;
1186+
__weak RCTSparseArray *weakScheduledCallbacks = _scheduledCallbacks;
1187+
1188+
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
1189+
RCTProfileBeginEvent();
1190+
1191+
NSMutableArray *scheduledCalls = weakScheduledCalls;
1192+
RCTSparseArray *scheduledCallbacks = weakScheduledCallbacks;
1193+
if (!scheduledCalls || !scheduledCallbacks) {
1194+
return;
1195+
}
1196+
1197+
/**
1198+
* Event deduping
1199+
*
1200+
* Right now we make a lot of assumptions about the arguments structure
1201+
* so just iterate if it's a `callFunctionReturnFlushedQueue()`
1202+
*/
1203+
if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) {
1204+
NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]];
1205+
/**
1206+
* Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter
1207+
*/
1208+
if ([moduleName hasSuffix:@"EventEmitter"]) {
1209+
for (NSDictionary *call in [scheduledCalls copy]) {
1210+
NSArray *callArgs = call[@"args"];
1211+
/**
1212+
* If it's the same module && method call on the bridge &&
1213+
* the same EventEmitter module && method
1214+
*/
1215+
if (
1216+
[call[@"module"] isEqualToString:module] &&
1217+
[call[@"method"] isEqualToString:method] &&
1218+
[callArgs[0] isEqual:args[0]] &&
1219+
[callArgs[1] isEqual:args[1]]
1220+
) {
1221+
/**
1222+
* args[2] contains the actual arguments for the event call, where
1223+
* args[2][0] is the target for RCTEventEmitter or the eventName
1224+
* for the other EventEmitters
1225+
* if RCTEventEmitter we need to compare args[2][1] that will be
1226+
* the eventName
1227+
*/
1228+
if (
1229+
[args[2][0] isEqual:callArgs[2][0]] &&
1230+
([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES)
1231+
) {
1232+
[scheduledCalls removeObject:call];
1233+
}
1234+
}
1235+
}
11751236
}
11761237
}
1177-
}
11781238

1179-
id call = @{
1180-
@"module": module,
1181-
@"method": method,
1182-
@"args": args,
1183-
@"context": context ?: @0,
1184-
};
1239+
id call = @{
1240+
@"module": module,
1241+
@"method": method,
1242+
@"args": args,
1243+
@"context": context ?: @0,
1244+
};
11851245

1186-
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
1187-
_scheduledCallbacks[args[0]] = call;
1188-
} else {
1189-
[_scheduledCalls addObject:call];
1190-
}
1246+
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
1247+
scheduledCallbacks[args[0]] = call;
1248+
} else {
1249+
[scheduledCalls addObject:call];
1250+
}
11911251

1192-
RCTProfileEndEvent(@"enqueue_call", @"objc_call", call);
1252+
RCTProfileEndEvent(@"enqueue_call", @"objc_call", call);
1253+
}];
11931254
}
11941255

11951256
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context

React/Base/RCTJavaScriptExecutor.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
4949
*/
5050
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block;
5151

52+
@optional
53+
54+
/**
55+
* Special case for Timers + ContextExecutor - instead of the default
56+
* if jsthread then call else dispatch call on jsthread
57+
* ensure the call is made async on the jsthread
58+
*/
59+
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block;
60+
5261
@end
5362

5463
static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID";

React/Executors/RCTContextExecutor.m

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,20 @@ - (void)executeApplicationScript:(NSString *)script
312312

313313
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
314314
{
315-
/**
316-
* Always dispatch async, ensure there are no sync calls on the JS thread
317-
* otherwise timers can cause a deadlock
318-
*/
319-
[self performSelector:@selector(_runBlock:)
320-
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
315+
if ([NSThread currentThread] != _javaScriptThread) {
316+
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
317+
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
318+
} else {
319+
block();
320+
}
321+
}
322+
323+
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block
324+
{
325+
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
326+
onThread:_javaScriptThread
327+
withObject:block
328+
waitUntilDone:NO];
321329
}
322330

323331
- (void)_runBlock:(dispatch_block_t)block

0 commit comments

Comments
 (0)