From 867329480632344ff2159100e40ef871bd388cf2 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 13 Mar 2025 11:52:26 -0700 Subject: [PATCH] Add option for deterministic Animated batching on iOS (#49942) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49942 iOS flushes NativeAnimated operations to the UI thread in one of two ways: 1. Some operations flush immediately when the operation is queued, namely startAnimation, stopAnimation, and setValue. 2. Other operations rely on flushing from one of the previous operation types, or a UIManager mount callback. This behavior leads to non-determinism in flushing of some operations like `setOffset` or `startListeningToAnimatedNodeValue`, where if these operations are created, out-of-band from "flushed" operations, they will not take affect until the next React commit. This change wires up the `animatedShouldSignalBatch` feature flag to queue operations on the JS thread, and flush them after `NativeAnimated.finishOperationBatch` is called. ## Changelog [Internal] Reviewed By: javache Differential Revision: D70904592 fbshipit-source-id: 6bc7a8614cbe5e275d4d7bf5310da9f0090c0e10 --- .../RCTNativeAnimatedTurboModule.mm | 242 +++++++++--------- .../React-RCTAnimation.podspec | 1 + 2 files changed, 121 insertions(+), 122 deletions(-) diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm index a3f8d1583cf..5f0988e1380 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import "RCTAnimationPlugins.h" @@ -73,52 +74,42 @@ RCT_EXPORT_MODULE(); #pragma mark-- API -RCT_EXPORT_METHOD(startOperationBatch) -{ - // TODO T71377585 -} +RCT_EXPORT_METHOD(startOperationBatch) {} RCT_EXPORT_METHOD(finishOperationBatch) { - // TODO T71377585 + // This method is only called from JS when animatedShouldSignalBatch is enabled. + [self flushOperationQueues]; } RCT_EXPORT_METHOD(createAnimatedNode : (double)tag config : (NSDictionary *)config) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager createAnimatedNode:[NSNumber numberWithDouble:tag] config:config]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager createAnimatedNode:[NSNumber numberWithDouble:tag] config:config]; + }]; } RCT_EXPORT_METHOD(updateAnimatedNodeConfig : (double)tag config : (NSDictionary *)config) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager updateAnimatedNodeConfig:[NSNumber numberWithDouble:tag] config:config]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager updateAnimatedNodeConfig:[NSNumber numberWithDouble:tag] config:config]; + }]; } RCT_EXPORT_METHOD(connectAnimatedNodes : (double)parentTag childTag : (double)childTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager connectAnimatedNodes:[NSNumber numberWithDouble:parentTag] - childTag:[NSNumber numberWithDouble:childTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager connectAnimatedNodes:[NSNumber numberWithDouble:parentTag] + childTag:[NSNumber numberWithDouble:childTag]]; + }]; } RCT_EXPORT_METHOD(disconnectAnimatedNodes : (double)parentTag childTag : (double)childTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager disconnectAnimatedNodes:[NSNumber numberWithDouble:parentTag] - childTag:[NSNumber numberWithDouble:childTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager disconnectAnimatedNodes:[NSNumber numberWithDouble:parentTag] + childTag:[NSNumber numberWithDouble:childTag]]; + }]; } RCT_EXPORT_METHOD(startAnimatingNode @@ -127,126 +118,94 @@ RCT_EXPORT_METHOD(startAnimatingNode : (NSDictionary *)config endCallback : (RCTResponseSenderBlock)callBack) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager startAnimatingNode:[NSNumber numberWithDouble:animationId] - nodeTag:[NSNumber numberWithDouble:nodeTag] - config:config - endCallback:callBack]; - }]; - - [self flushOperationQueues]; - }); + [self queueFlushedOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager startAnimatingNode:[NSNumber numberWithDouble:animationId] + nodeTag:[NSNumber numberWithDouble:nodeTag] + config:config + endCallback:callBack]; + }]; } RCT_EXPORT_METHOD(stopAnimation : (double)animationId) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager stopAnimation:[NSNumber numberWithDouble:animationId]]; - }]; - [self flushOperationQueues]; - }); + [self queueFlushedOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager stopAnimation:[NSNumber numberWithDouble:animationId]]; + }]; } RCT_EXPORT_METHOD(setAnimatedNodeValue : (double)nodeTag value : (double)value) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager setAnimatedNodeValue:[NSNumber numberWithDouble:nodeTag] value:[NSNumber numberWithDouble:value]]; - }]; - // In Bridge, flushing of native animations is done from RCTCxxBridge batchDidComplete(). - // Since RCTCxxBridge doesn't exist in Bridgeless, and components are not remounted in Fabric for native animations, - // flush here for changes in Animated.Value for Animated.event. - [self flushOperationQueues]; - }); + [self queueFlushedOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager setAnimatedNodeValue:[NSNumber numberWithDouble:nodeTag] value:[NSNumber numberWithDouble:value]]; + }]; } RCT_EXPORT_METHOD(setAnimatedNodeOffset : (double)nodeTag offset : (double)offset) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager setAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag] - offset:[NSNumber numberWithDouble:offset]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager setAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag] offset:[NSNumber numberWithDouble:offset]]; + }]; } RCT_EXPORT_METHOD(flattenAnimatedNodeOffset : (double)nodeTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager flattenAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager flattenAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag]]; + }]; } RCT_EXPORT_METHOD(extractAnimatedNodeOffset : (double)nodeTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager extractAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager extractAnimatedNodeOffset:[NSNumber numberWithDouble:nodeTag]]; + }]; } RCT_EXPORT_METHOD(connectAnimatedNodeToView : (double)nodeTag viewTag : (double)viewTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - // viewName is not used when node is managed by Fabric, and nodes are always managed by Fabric in Bridgeless. - [nodesManager connectAnimatedNodeToView:[NSNumber numberWithDouble:nodeTag] - viewTag:[NSNumber numberWithDouble:viewTag] - viewName:nil]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + // viewName is not used when node is managed by Fabric, and nodes are always managed by Fabric in Bridgeless. + [nodesManager connectAnimatedNodeToView:[NSNumber numberWithDouble:nodeTag] + viewTag:[NSNumber numberWithDouble:viewTag] + viewName:nil]; + }]; } RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView : (double)nodeTag viewTag : (double)viewTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager disconnectAnimatedNodeFromView:[NSNumber numberWithDouble:nodeTag] - viewTag:[NSNumber numberWithDouble:viewTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager disconnectAnimatedNodeFromView:[NSNumber numberWithDouble:nodeTag] + viewTag:[NSNumber numberWithDouble:viewTag]]; + }]; } RCT_EXPORT_METHOD(restoreDefaultValues : (double)nodeTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addPreOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager restoreDefaultValues:[NSNumber numberWithDouble:nodeTag]]; - }]; - }); + [self queuePreOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager restoreDefaultValues:[NSNumber numberWithDouble:nodeTag]]; + }]; } RCT_EXPORT_METHOD(dropAnimatedNode : (double)tag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager dropAnimatedNode:[NSNumber numberWithDouble:tag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager dropAnimatedNode:[NSNumber numberWithDouble:tag]]; + }]; } RCT_EXPORT_METHOD(startListeningToAnimatedNodeValue : (double)tag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - __weak id valueObserver = self; - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager startListeningToAnimatedNodeValue:[NSNumber numberWithDouble:tag] valueObserver:valueObserver]; - }]; - }); + __weak id valueObserver = self; + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager startListeningToAnimatedNodeValue:[NSNumber numberWithDouble:tag] valueObserver:valueObserver]; + }]; } RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue : (double)tag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager stopListeningToAnimatedNodeValue:[NSNumber numberWithDouble:tag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager stopListeningToAnimatedNodeValue:[NSNumber numberWithDouble:tag]]; + }]; } RCT_EXPORT_METHOD(addAnimatedEventToView @@ -261,13 +220,11 @@ RCT_EXPORT_METHOD(addAnimatedEventToView eventMappingDict[@"animatedValueTag"] = @(*eventMapping.animatedValueTag()); } - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager addAnimatedEventToView:[NSNumber numberWithDouble:viewTag] - eventName:eventName - eventMapping:eventMappingDict]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager addAnimatedEventToView:[NSNumber numberWithDouble:viewTag] + eventName:eventName + eventMapping:eventMappingDict]; + }]; } RCT_EXPORT_METHOD(removeAnimatedEventFromView @@ -275,22 +232,18 @@ RCT_EXPORT_METHOD(removeAnimatedEventFromView : (nonnull NSString *)eventName animatedNodeTag : (double)animatedNodeTag) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager removeAnimatedEventFromView:[NSNumber numberWithDouble:viewTag] - eventName:eventName - animatedNodeTag:[NSNumber numberWithDouble:animatedNodeTag]]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager removeAnimatedEventFromView:[NSNumber numberWithDouble:viewTag] + eventName:eventName + animatedNodeTag:[NSNumber numberWithDouble:animatedNodeTag]]; + }]; } RCT_EXPORT_METHOD(getValue : (double)nodeTag saveValueCallback : (RCTResponseSenderBlock)saveValueCallback) { - dispatch_async(RCTGetUIManagerQueue(), ^{ - [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { - [nodesManager getValue:[NSNumber numberWithDouble:nodeTag] saveCallback:saveValueCallback]; - }]; - }); + [self queueOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { + [nodesManager getValue:[NSNumber numberWithDouble:nodeTag] saveCallback:saveValueCallback]; + }]; } RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArgs) @@ -300,6 +253,43 @@ RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArg #pragma mark-- Batch handling +- (void)queueFlushedOperationBlock:(AnimatedOperation)operation +{ + if (facebook::react::ReactNativeFeatureFlags::animatedShouldSignalBatch()) { + [self addOperationBlock:operation]; + } else { + dispatch_async(RCTGetUIManagerQueue(), ^{ + [self addOperationBlock:operation]; + // In Bridge, flushing of native animations is done from RCTCxxBridge batchDidComplete(). + // Since RCTCxxBridge doesn't exist in Bridgeless, and components are not remounted in Fabric for native + // animations, flush here for changes in Animated.Value for Animated.event. + [self flushOperationQueues]; + }); + } +} + +- (void)queueOperationBlock:(AnimatedOperation)operation +{ + if (facebook::react::ReactNativeFeatureFlags::animatedShouldSignalBatch()) { + [self addOperationBlock:operation]; + } else { + dispatch_async(RCTGetUIManagerQueue(), ^{ + [self addOperationBlock:operation]; + }); + } +} + +- (void)queuePreOperationBlock:(AnimatedOperation)operation +{ + if (facebook::react::ReactNativeFeatureFlags::animatedShouldSignalBatch()) { + [self addPreOperationBlock:operation]; + } else { + dispatch_async(RCTGetUIManagerQueue(), ^{ + [self addPreOperationBlock:operation]; + }); + } +} + - (void)addOperationBlock:(AnimatedOperation)operation { [_operations addObject:operation]; @@ -335,6 +325,10 @@ RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArg - (void)willMountComponentsWithRootTag:(NSInteger)rootTag { + if (facebook::react::ReactNativeFeatureFlags::animatedShouldSignalBatch()) { + return; + } + RCTAssertMainQueue(); RCTExecuteOnUIManagerQueue(^{ NSArray *preOperations = self->_preOperations; @@ -350,6 +344,10 @@ RCT_EXPORT_METHOD(queueAndExecuteBatchedOperations : (NSArray *)operationsAndArg - (void)didMountComponentsWithRootTag:(NSInteger)rootTag { + if (facebook::react::ReactNativeFeatureFlags::animatedShouldSignalBatch()) { + return; + } + RCTAssertMainQueue(); RCTExecuteOnUIManagerQueue(^{ NSArray *operations = self->_operations; diff --git a/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec b/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec index aa2d6f16cd6..aeb8c355c5c 100644 --- a/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec +++ b/packages/react-native/Libraries/NativeAnimation/React-RCTAnimation.podspec @@ -47,6 +47,7 @@ Pod::Spec.new do |s| add_dependency(s, "React-RCTFBReactNativeSpec") add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) add_dependency(s, "React-NativeModulesApple") + add_dependency(s, "React-featureflags") add_rn_third_party_dependencies(s) end