From 159d395ac320bec74634dd9d45332d6683340363 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Mon, 15 Jun 2026 07:41:15 -0700 Subject: [PATCH] Decide NativeAnimated backend per-instance instead of live flag (#57205) Summary: ## Changelog: [Internal][Fixed] - Fix EXC_BAD_ACCESS / KERN_INVALID_ADDRESS in C++ Animated when `useSharedAnimatedBackend()` flips across a reused runtime C++ Animated chose between the legacy and shared-`AnimationBackend` code paths by reading `ReactNativeFeatureFlags::useSharedAnimatedBackend()` live, in many places. That flag is a process-global singleton (`ReactNativeFeatureFlags::accessor_`). On some app the global is reset and re-applied on every user switch (`FBReactModule setUpReactNativeFeatureFlags` -> `dangerouslyReset()`/`override()`), and the previous user's runtime is kept alive and reused. The shared `AnimationBackend` is attached only once, when an instance's `Scheduler` is constructed, gated on the flag at that moment. On a multi-account device the global flag could therefore read true on a reused instance whose backend was never attached, so `getOrCreate` took the shared path and dereferenced a null backend. It also let JS and C++ disagree, since JS caches the flag per runtime while C++ followed the mutated global. Fix: make the per-instance decision once and use it everywhere instead of the live flag. - `NativeAnimatedNodesManagerProvider::getOrCreate` selects the path by whether the shared `AnimationBackend` actually exists for this instance (`unstable_getAnimationBackend().lock() != nullptr`). - `NativeAnimatedNodesManager` stores a `const bool useSharedAnimatedBackend_`, latched in its constructor (true for the shared-backend ctor, false for the legacy ctor), and exposes it via `useSharedAnimatedBackend()`. All internal reads now use the member. - `PropsAnimatedNode` reads the decision through `manager_->useSharedAnimatedBackend()`. The attach side (`Scheduler`) is intentionally unchanged: it remains the single construction-time read that latches the per-instance decision the rest of the code now follows. This keeps JS and C++ consistent across global flag flips and removes the null dereference. Reviewed By: sbuggay Differential Revision: D108428720 --- .../animated/NativeAnimatedNodesManager.cpp | 19 ++++++++++--------- .../animated/NativeAnimatedNodesManager.h | 14 ++++++++++++++ .../NativeAnimatedNodesManagerProvider.cpp | 15 ++++++--------- .../animated/nodes/PropsAnimatedNode.cpp | 5 ++--- .../api-snapshots/ReactAndroidDebugCxx.api | 1 + .../api-snapshots/ReactAndroidNewarchCxx.api | 1 + .../api-snapshots/ReactAndroidReleaseCxx.api | 1 + .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + .../api-snapshots/ReactCommonDebugCxx.api | 1 + .../api-snapshots/ReactCommonNewarchCxx.api | 1 + .../api-snapshots/ReactCommonReleaseCxx.api | 1 + 13 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp index 5eeb0ca77a4c..363c49a0cba3 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -73,7 +73,8 @@ NativeAnimatedNodesManager::NativeAnimatedNodesManager( StartOnRenderCallback&& startOnRenderCallback, StopOnRenderCallback&& stopOnRenderCallback, FrameRateListenerCallback&& frameRateListenerCallback) noexcept - : directManipulationCallback_(std::move(directManipulationCallback)), + : useSharedAnimatedBackend_(false), + directManipulationCallback_(std::move(directManipulationCallback)), fabricCommitCallback_(std::move(fabricCommitCallback)), resolvePlatformColor_(std::move(resolvePlatformColor)), startOnRenderCallback_(std::move(startOnRenderCallback)), @@ -97,7 +98,7 @@ NativeAnimatedNodesManager::NativeAnimatedNodesManager( NativeAnimatedNodesManager::NativeAnimatedNodesManager( std::shared_ptr animationBackend) noexcept - : animationBackend_(animationBackend) {} + : animationBackend_(animationBackend), useSharedAnimatedBackend_(true) {} NativeAnimatedNodesManager::~NativeAnimatedNodesManager() noexcept { stopRenderCallbackIfNeeded(true); @@ -256,7 +257,7 @@ void NativeAnimatedNodesManager::disconnectAnimatedNodeFromView( auto node = getAnimatedNode(propsNodeTag); if (node != nullptr) { node->disconnectFromView(viewTag); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { node->disconnectFromShadowNodeFamily(); } { @@ -521,7 +522,7 @@ void NativeAnimatedNodesManager::handleAnimatedEvent( // That's why, in case this is called from the UI thread, we need to // proactivelly trigger the animation loop to avoid showing stale // frames. - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { if (auto animationBackend = animationBackend_.lock()) { animationBackend->pushAnimationMutations( [this](AnimationTimestamp timestamp) -> AnimationMutations { @@ -561,7 +562,7 @@ void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) { return; } - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { if (auto animationBackend = animationBackend_.lock()) { auto weak = weak_from_this(); animationBackendCallbackId_ = animationBackend->start( @@ -589,7 +590,7 @@ void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded( // stopRenderCallbackIfNeeded is always called from the UI thread. auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { if (isRenderCallbackStarted) { if (auto animationBackend = animationBackend_.lock()) { animationBackend->stop(animationBackendCallbackId_); @@ -922,7 +923,7 @@ void NativeAnimatedNodesManager::schedulePropsCommit( bool layoutStyleUpdated, bool forceFabricCommit, ShadowNodeFamily::Weak shadowNodeFamily) noexcept { - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { if (forceFabricCommit) { shouldRequestAsyncFlush_.insert(viewTag); } @@ -1029,7 +1030,7 @@ AnimationMutations NativeAnimatedNodesManager::onAnimationFrameForBackend( AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations( AnimationTimestamp timestamp) { - if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (!useSharedAnimatedBackend_) { return {}; } TraceSection s( @@ -1119,7 +1120,7 @@ void NativeAnimatedNodesManager::flushAnimatedNodesCreatedAsync() noexcept { } void NativeAnimatedNodesManager::onRender() { - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (useSharedAnimatedBackend_) { return; } TraceSection s( diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h index 67a619f2552c..26a8209a61d2 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h @@ -78,6 +78,14 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this>> T *getAnimatedNode(Tag tag) const requires(std::is_base_of_v) @@ -219,6 +227,12 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this animationBackend_; + // Latched per-instance copy of which backend this manager uses, set from the + // constructor that ran (true for the shared-AnimationBackend ctor). Reads stay + // stable even when the global useSharedAnimatedBackend() flag is re-overridden + // on another RN runtime. + const bool useSharedAnimatedBackend_; + std::unique_ptr animatedNode(Tag tag, const folly::dynamic &config) noexcept; static thread_local bool isOnRenderThread_; diff --git a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp index 95d3a508521e..26bea6f2d7c9 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManagerProvider.cpp @@ -8,7 +8,6 @@ #include "NativeAnimatedNodesManagerProvider.h" #include -#include #include #include #include @@ -51,8 +50,9 @@ NativeAnimatedNodesManagerProvider::getOrCreate( auto* uiManager = &UIManagerBinding::getBinding(runtime)->getUIManager(); - if (!ReactNativeFeatureFlags::useSharedAnimatedBackend()) { - // === PATH 1: Legacy Backend (useSharedAnimatedBackend = false) === + auto animationBackend = uiManager->unstable_getAnimationBackend().lock(); + if (animationBackend == nullptr) { + // === PATH 1: Legacy Backend (no shared AnimationBackend attached) === // Uses the architecture with MergedValueDispatcher and // AnimatedMountingOverrideDelegate @@ -130,13 +130,10 @@ NativeAnimatedNodesManagerProvider::getOrCreate( uiManager->setNativeAnimatedDelegate(nativeAnimatedDelegate_); } else { - // === PATH 2: Shared AnimationBackend (useSharedAnimatedBackend = true) === + // === PATH 2: Shared AnimationBackend === // Uses the shared AnimationBackend from UIManager. The backend handles all - // animation commits and platform integration internally. - - auto animationBackend = uiManager->unstable_getAnimationBackend().lock(); - react_native_assert( - animationBackend != nullptr && "animationBackend is nullptr"); + // animation commits and platform integration internally. It is guaranteed + // non-null here because it was successfully locked above. animationBackend->registerJSInvoker(jsInvoker); nativeAnimatedNodesManager_ = diff --git a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp index 5cee6b457e4a..b47bbd26fe3b 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/nodes/PropsAnimatedNode.cpp @@ -12,7 +12,6 @@ #include "PropsAnimatedNode.h" #include -#include #include #include #include @@ -84,7 +83,7 @@ void PropsAnimatedNode::disconnectFromView(Tag viewTag) { void PropsAnimatedNode::restoreDefaultValues() { // If node is already disconnected from View, we cannot restore default values if (connectedViewTag_ != animated::undefinedAnimatedNodeIdentifier) { - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (manager_->useSharedAnimatedBackend()) { manager_->schedulePropsCommit( connectedViewTag_, folly::dynamic::object(), @@ -166,7 +165,7 @@ void PropsAnimatedNode::update(bool forceFabricCommit) { layoutStyleUpdated_ = isLayoutStyleUpdated(getConfig()["props"], *manager_); - if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { + if (manager_->useSharedAnimatedBackend()) { manager_->schedulePropsCommit( connectedViewTag_, props_, diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index c58793426e3a..3eb4614f8ed8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -3491,6 +3491,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index c6855293991d..8d27eff78eb4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3375,6 +3375,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index e39de8ef0e33..ce9e2229b721 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -3488,6 +3488,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e4afb026c956..323f80e5161f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5710,6 +5710,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 8b369a29aee5..0e9f7bf4394e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -5622,6 +5622,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 2dcd88a4283f..270a0c797f5e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5707,6 +5707,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 7bb8f3c808d3..39c05b7ffbe5 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2325,6 +2325,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 11d1e36952ef..e0293739574a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2249,6 +2249,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 6d7c15d11ef2..0ccad701dc85 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2322,6 +2322,7 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr public bool commitProps(); public bool hasManagedProps() const noexcept; public bool isOnRenderThread() const noexcept; + public bool useSharedAnimatedBackend() const noexcept; public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp); public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp); public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete;