diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 539d1e65255415..8f3419ba4e20a5 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2920,6 +2920,7 @@ public abstract interface class com/facebook/react/fabric/mounting/mountitems/Mo public final class com/facebook/react/fabric/mounting/mountitems/MountItemFactory { public static final field INSTANCE Lcom/facebook/react/fabric/mounting/mountitems/MountItemFactory; + public static final fun createDestroyViewMountItem (II)Lcom/facebook/react/fabric/mounting/mountitems/MountItem; public static final fun createDispatchCommandMountItem (IIILcom/facebook/react/bridge/ReadableArray;)Lcom/facebook/react/fabric/mounting/mountitems/DispatchCommandMountItem; public static final fun createDispatchCommandMountItem (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)Lcom/facebook/react/fabric/mounting/mountitems/DispatchCommandMountItem; public static final fun createIntBufferBatchMountItem (I[I[Ljava/lang/Object;I)Lcom/facebook/react/fabric/mounting/mountitems/MountItem; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index bd092593672710..870544b54d62c5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -760,6 +760,14 @@ private void preallocateView( isLayoutable)); } + @SuppressWarnings("unused") + @AnyThread + @ThreadConfined(ANY) + private void destroyUnmountedView(int surfaceId, int reactTag) { + mMountItemDispatcher.addMountItem( + MountItemFactory.createDestroyViewMountItem(surfaceId, reactTag)); + } + @SuppressLint("NotInvokedPrivateMethod") @SuppressWarnings("unused") @AnyThread diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DestroyUnmountedViewMountItem.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DestroyUnmountedViewMountItem.kt new file mode 100644 index 00000000000000..1096dd86b8a813 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/DestroyUnmountedViewMountItem.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.fabric.mounting.mountitems + +import com.facebook.react.fabric.mounting.MountingManager + +/** + * Destroyes the view asociated to the [reactTag] if exists. This MountItem is meant to be used ONLY + * for views that were preallcated but never mounted on the screen. + */ +internal class DestroyUnmountedViewMountItem( + private val _surfaceId: Int, + private val reactTag: Int +) : MountItem { + + public override fun execute(mountingManager: MountingManager) { + val surfaceMountingManager = mountingManager.getSurfaceManager(_surfaceId) + if (surfaceMountingManager == null) { + return + } + surfaceMountingManager.deleteView(reactTag) + } + + public override fun getSurfaceId(): Int = _surfaceId +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/MountItemFactory.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/MountItemFactory.kt index f74ab4042ed4cb..d3e71cc05368ce 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/MountItemFactory.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/MountItemFactory.kt @@ -53,6 +53,11 @@ public object MountItemFactory { ): MountItem = PreAllocateViewMountItem(surfaceId, reactTag, component, props, stateWrapper, isLayoutable) + /** @return a [MountItem] that will be used to destroy views */ + @JvmStatic + public fun createDestroyViewMountItem(surfaceId: Int, reactTag: Int): MountItem = + DestroyUnmountedViewMountItem(surfaceId, reactTag) + /** * @return a [MountItem] that will be read and execute a collection of MountItems serialized in * the int[] and Object[] received by parameter diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index 3332d683ba6ef7..b3915ff3afe8b6 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -532,6 +532,19 @@ void Binding::schedulerDidRequestPreliminaryViewAllocation( return; } mountingManager->maybePreallocateShadowNode(shadowNode); + // Only the Views of ShadowNode that were pre-allocated (forms views) needs + // to be destroyed if the ShadowNode is destroyed but it was never mounted + // on the screen. + if (ReactNativeFeatureFlags::enableDeletionOfUnmountedViews() && + shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) { + shadowNode.getFamily().onUnmountedFamilyDestroyed( + [weakMountingManager = + std::weak_ptr(mountingManager)](const ShadowNodeFamily& family) { + if (auto mountingManager = weakMountingManager.lock()) { + mountingManager->destroyUnmountedShadowNode(family); + } + }); + } } void Binding::schedulerDidDispatchCommand( diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index bec4f2c42bf7e3..36f23bee043c59 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -792,6 +792,21 @@ void FabricMountingManager::drainPreallocateViewsQueue() { } } +void FabricMountingManager::destroyUnmountedShadowNode( + const ShadowNodeFamily& family) { + auto tag = family.getTag(); + auto surfaceId = family.getSurfaceId(); + + // ThreadScope::WithClassLoader is necessary because + // destroyUnmountedShadowNode is being called from a destructor thread + facebook::jni::ThreadScope::WithClassLoader([&]() { + static auto destroyUnmountedView = + JFabricUIManager::javaClassStatic()->getMethod( + "destroyUnmountedView"); + destroyUnmountedView(javaUIManager_, surfaceId, tag); + }); +} + void FabricMountingManager::maybePreallocateShadowNode( const ShadowNode& shadowNode) { if (!shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) { @@ -813,7 +828,7 @@ void FabricMountingManager::maybePreallocateShadowNode( preallocatedViewsQueue_.push_back(std::move(shadowView)); } else { // Old implementation where FabricUIManager.preallocateView is called - // immediatelly. + // immediately. preallocateShadowView(shadowView); } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 0cc42f23f7b1ec..afafc989ab3736 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -34,6 +34,8 @@ class FabricMountingManager final { void maybePreallocateShadowNode(const ShadowNode& shadowNode); + void destroyUnmountedShadowNode(const ShadowNodeFamily& family); + /* * Drains preallocatedViewsQueue_ by calling preallocateShadowView on each * item in the queue. Can be called by any thread. diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp index 44cca7436b984f..9bcedf901cd03e 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -282,6 +282,7 @@ void ShadowNode::cloneChildrenIfShared() { void ShadowNode::setMounted(bool mounted) const { if (mounted) { family_->setMostRecentState(getState()); + family_->setMounted(); hasBeenMounted_ = mounted; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp index 0e7fbdc2ca47ab..d9cd860098cbc6 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp @@ -9,6 +9,7 @@ #include "ShadowNode.h" #include +#include #include #include @@ -58,10 +59,30 @@ ComponentName ShadowNodeFamily::getComponentName() const { return componentName_; } +void ShadowNodeFamily::setMounted() const { + hasBeenMounted_ = true; +} + const ComponentDescriptor& ShadowNodeFamily::getComponentDescriptor() const { return componentDescriptor_; } +void ShadowNodeFamily::onUnmountedFamilyDestroyed( + std::function callback) const { + onUnmountedFamilyDestroyedCallback_ = std::move(callback); +} + +Tag ShadowNodeFamily::getTag() const { + return tag_; +} + +ShadowNodeFamily::~ShadowNodeFamily() { + if (ReactNativeFeatureFlags::enableDeletionOfUnmountedViews() && + !hasBeenMounted_ && onUnmountedFamilyDestroyedCallback_ != nullptr) { + onUnmountedFamilyDestroyedCallback_(*this); + } +} + AncestorList ShadowNodeFamily::getAncestors( const ShadowNode& ancestorShadowNode) const { auto families = std::vector{}; diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h index faa44d74443efb..c06213774192dd 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h @@ -25,7 +25,7 @@ class State; * `ShadowNodeFamily` instances. * * Do not use this class as a general purpose container to share information - * about a `ShadowNodeFamily`. Pelase define specific purpose containers in + * about a `ShadowNodeFamily`. Please define specific purpose containers in * those cases. * */ @@ -87,12 +87,24 @@ class ShadowNodeFamily final { SharedEventEmitter getEventEmitter() const; + /** + * @param callback will be executed when an unmounted instance of + * ShadowNodeFamily is destroyed. + */ + void onUnmountedFamilyDestroyed( + std::function callback) const; + /* * Sets and gets the most recent state. */ std::shared_ptr getMostRecentState() const; void setMostRecentState(const std::shared_ptr& state) const; + /** + * Mark this ShadowNodeFamily as mounted. + */ + void setMounted() const; + /* * Dispatches a state update with given priority. */ @@ -105,6 +117,17 @@ class ShadowNodeFamily final { */ mutable std::unique_ptr nativeProps_DEPRECATED; + /** + * @return tag for the ShadowNodeFamily. + */ + Tag getTag() const; + + /** + * Override destructor to call onUnmountedFamilyDestroyedCallback() for + * ShadowViews that were preallocated but never mounted on the screen. + */ + ~ShadowNodeFamily(); + private: friend ShadowNode; friend State; @@ -121,6 +144,9 @@ class ShadowNodeFamily final { mutable std::shared_ptr mostRecentState_; mutable std::shared_mutex mutex_; + mutable std::function + onUnmountedFamilyDestroyedCallback_ = nullptr; + /* * Deprecated. */ @@ -165,6 +191,11 @@ class ShadowNodeFamily final { * For optimization purposes only. */ mutable bool hasParent_{false}; + + /* + * Determines if the ShadowNodeFamily was ever mounted on the screen. + */ + mutable bool hasBeenMounted_{false}; }; } // namespace facebook::react