diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 4a14713c7..5b6a92682 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -672,6 +672,8 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) */ static NSMutableDictionary *RCTLocalModuleIDs; static NSMutableDictionary *RCTLocalMethodIDs; +static NSMutableArray *RCTLocalModuleNames; +static NSMutableArray *RCTLocalMethodNames; static NSDictionary *RCTLocalModulesConfig() { static NSMutableDictionary *localModules; @@ -680,6 +682,8 @@ static NSDictionary *RCTLocalModulesConfig() RCTLocalModuleIDs = [[NSMutableDictionary alloc] init]; RCTLocalMethodIDs = [[NSMutableDictionary alloc] init]; + RCTLocalModuleNames = [[NSMutableArray alloc] init]; + RCTLocalMethodNames = [[NSMutableArray alloc] init]; localModules = [[NSMutableDictionary alloc] init]; for (NSString *moduleDotMethod in RCTJSMethods()) { @@ -711,6 +715,8 @@ static NSDictionary *RCTLocalModulesConfig() // Add module and method lookup RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"]; RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"]; + [RCTLocalModuleNames addObject:moduleName]; + [RCTLocalMethodNames addObject:methodName]; } }); @@ -1072,7 +1078,7 @@ static id _latestJSExecutor; #pragma mark - RCTBridge methods /** - * Like JS::call, for objective-c. + * Public. Can be invoked from any thread. */ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args { @@ -1083,12 +1089,10 @@ static id _latestJSExecutor; NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - if (!_loading) { - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, args ?: @[]] - context:RCTGetExecutorID(_javaScriptExecutor)]; - } + [self _invokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, args ?: @[]] + context:RCTGetExecutorID(_javaScriptExecutor)]; } /** @@ -1106,10 +1110,17 @@ static id _latestJSExecutor; if (!_loading) { #if BATCHED_BRIDGE - [self _actuallyInvokeAndProcessModule:@"BatchedBridge" - method:@"callFunctionReturnFlushedQueue" - arguments:@[moduleID, methodID, @[@[timer]]] - context:RCTGetExecutorID(_javaScriptExecutor)]; + dispatch_block_t block = ^{ + [self _actuallyInvokeAndProcessModule:@"BatchedBridge" + method:@"callFunctionReturnFlushedQueue" + arguments:@[moduleID, methodID, @[@[timer]]] + context:RCTGetExecutorID(_javaScriptExecutor)]; + }; + if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) { + [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block]; + } else { + [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + } #else @@ -1163,33 +1174,83 @@ static id _latestJSExecutor; } } +/** + * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer, + * on the JS thread, but only in non-batched mode. + */ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { #if BATCHED_BRIDGE - RCTProfileBeginEvent(); - if ([module isEqualToString:@"RCTEventEmitter"]) { - for (NSDictionary *call in _scheduledCalls) { - if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) { - [_scheduledCalls removeObject:call]; + __weak NSMutableArray *weakScheduledCalls = _scheduledCalls; + __weak RCTSparseArray *weakScheduledCallbacks = _scheduledCallbacks; + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileBeginEvent(); + + NSMutableArray *scheduledCalls = weakScheduledCalls; + RCTSparseArray *scheduledCallbacks = weakScheduledCallbacks; + if (!scheduledCalls || !scheduledCallbacks) { + return; + } + + /** + * Event deduping + * + * Right now we make a lot of assumptions about the arguments structure + * so just iterate if it's a `callFunctionReturnFlushedQueue()` + */ + if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) { + NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]]; + /** + * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter + */ + if ([moduleName hasSuffix:@"EventEmitter"]) { + for (NSDictionary *call in [scheduledCalls copy]) { + NSArray *callArgs = call[@"args"]; + /** + * If it's the same module && method call on the bridge && + * the same EventEmitter module && method + */ + if ( + [call[@"module"] isEqualToString:module] && + [call[@"method"] isEqualToString:method] && + [callArgs[0] isEqual:args[0]] && + [callArgs[1] isEqual:args[1]] + ) { + /** + * args[2] contains the actual arguments for the event call, where + * args[2][0] is the target for RCTEventEmitter or the eventName + * for the other EventEmitters + * if RCTEventEmitter we need to compare args[2][1] that will be + * the eventName + */ + if ( + [args[2][0] isEqual:callArgs[2][0]] && + ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES) + ) { + [scheduledCalls removeObject:call]; + } + } + } } } - } - id call = @{ - @"module": module, - @"method": method, - @"args": args, - @"context": context ?: @0, - }; + id call = @{ + @"module": module, + @"method": method, + @"args": args, + @"context": context ?: @0, + }; - if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { - _scheduledCallbacks[args[0]] = call; - } else { - [_scheduledCalls addObject:call]; - } + if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) { + scheduledCallbacks[args[0]] = call; + } else { + [scheduledCalls addObject:call]; + } - RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + }]; } - (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index eb7fd7d31..8f1eb8a98 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -49,6 +49,15 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); */ - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block; +@optional + +/** + * Special case for Timers + ContextExecutor - instead of the default + * if jsthread then call else dispatch call on jsthread + * ensure the call is made async on the jsthread + */ +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block; + @end static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID"; diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index f5089a9d6..412ffd256 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -312,12 +312,20 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - /** - * Always dispatch async, ensure there are no sync calls on the JS thread - * otherwise timers can cause a deadlock - */ - [self performSelector:@selector(_runBlock:) - onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } +} + +- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block +{ + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread + withObject:block + waitUntilDone:NO]; } - (void)_runBlock:(dispatch_block_t)block