Skip to content

Commit 6dbbbe3

Browse files
mdvaccafacebook-github-bot
authored andcommitted
Delete pre-allocated views that were never mounted on the screen (#46473)
Summary: Pull Request resolved: #46473 This diff extends the renderer of react native to ensure that pre-allocated views that were never mounted on the screen are deleted as soon as the shadow node is deleted from JS This feature is controlled by the ReactNativeFeatureFlag: enableDeletionOfUnmountedViews changelog: [internal] internal Reviewed By: javache Differential Revision: D62559190 fbshipit-source-id: 1af6785fc57256d12750db64489c9ecc6cf98c9d
1 parent 68a92aa commit 6dbbbe3

10 files changed

Lines changed: 129 additions & 2 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,7 @@ public abstract interface class com/facebook/react/fabric/mounting/mountitems/Mo
29202920

29212921
public final class com/facebook/react/fabric/mounting/mountitems/MountItemFactory {
29222922
public static final field INSTANCE Lcom/facebook/react/fabric/mounting/mountitems/MountItemFactory;
2923+
public static final fun createDestroyViewMountItem (II)Lcom/facebook/react/fabric/mounting/mountitems/MountItem;
29232924
public static final fun createDispatchCommandMountItem (IIILcom/facebook/react/bridge/ReadableArray;)Lcom/facebook/react/fabric/mounting/mountitems/DispatchCommandMountItem;
29242925
public static final fun createDispatchCommandMountItem (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)Lcom/facebook/react/fabric/mounting/mountitems/DispatchCommandMountItem;
29252926
public static final fun createIntBufferBatchMountItem (I[I[Ljava/lang/Object;I)Lcom/facebook/react/fabric/mounting/mountitems/MountItem;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,14 @@ private void preallocateView(
760760
isLayoutable));
761761
}
762762

763+
@SuppressWarnings("unused")
764+
@AnyThread
765+
@ThreadConfined(ANY)
766+
private void destroyUnmountedView(int surfaceId, int reactTag) {
767+
mMountItemDispatcher.addMountItem(
768+
MountItemFactory.createDestroyViewMountItem(surfaceId, reactTag));
769+
}
770+
763771
@SuppressLint("NotInvokedPrivateMethod")
764772
@SuppressWarnings("unused")
765773
@AnyThread
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.fabric.mounting.mountitems
9+
10+
import com.facebook.react.fabric.mounting.MountingManager
11+
12+
/**
13+
* Destroyes the view asociated to the [reactTag] if exists. This MountItem is meant to be used ONLY
14+
* for views that were preallcated but never mounted on the screen.
15+
*/
16+
internal class DestroyUnmountedViewMountItem(
17+
private val _surfaceId: Int,
18+
private val reactTag: Int
19+
) : MountItem {
20+
21+
public override fun execute(mountingManager: MountingManager) {
22+
val surfaceMountingManager = mountingManager.getSurfaceManager(_surfaceId)
23+
if (surfaceMountingManager == null) {
24+
return
25+
}
26+
surfaceMountingManager.deleteView(reactTag)
27+
}
28+
29+
public override fun getSurfaceId(): Int = _surfaceId
30+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/MountItemFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public object MountItemFactory {
5353
): MountItem =
5454
PreAllocateViewMountItem(surfaceId, reactTag, component, props, stateWrapper, isLayoutable)
5555

56+
/** @return a [MountItem] that will be used to destroy views */
57+
@JvmStatic
58+
public fun createDestroyViewMountItem(surfaceId: Int, reactTag: Int): MountItem =
59+
DestroyUnmountedViewMountItem(surfaceId, reactTag)
60+
5661
/**
5762
* @return a [MountItem] that will be read and execute a collection of MountItems serialized in
5863
* the int[] and Object[] received by parameter

packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,19 @@ void Binding::schedulerDidRequestPreliminaryViewAllocation(
532532
return;
533533
}
534534
mountingManager->maybePreallocateShadowNode(shadowNode);
535+
// Only the Views of ShadowNode that were pre-allocated (forms views) needs
536+
// to be destroyed if the ShadowNode is destroyed but it was never mounted
537+
// on the screen.
538+
if (ReactNativeFeatureFlags::enableDeletionOfUnmountedViews() &&
539+
shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) {
540+
shadowNode.getFamily().onUnmountedFamilyDestroyed(
541+
[weakMountingManager =
542+
std::weak_ptr(mountingManager)](const ShadowNodeFamily& family) {
543+
if (auto mountingManager = weakMountingManager.lock()) {
544+
mountingManager->destroyUnmountedShadowNode(family);
545+
}
546+
});
547+
}
535548
}
536549

537550
void Binding::schedulerDidDispatchCommand(

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,21 @@ void FabricMountingManager::drainPreallocateViewsQueue() {
792792
}
793793
}
794794

795+
void FabricMountingManager::destroyUnmountedShadowNode(
796+
const ShadowNodeFamily& family) {
797+
auto tag = family.getTag();
798+
auto surfaceId = family.getSurfaceId();
799+
800+
// ThreadScope::WithClassLoader is necessary because
801+
// destroyUnmountedShadowNode is being called from a destructor thread
802+
facebook::jni::ThreadScope::WithClassLoader([&]() {
803+
static auto destroyUnmountedView =
804+
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint)>(
805+
"destroyUnmountedView");
806+
destroyUnmountedView(javaUIManager_, surfaceId, tag);
807+
});
808+
}
809+
795810
void FabricMountingManager::maybePreallocateShadowNode(
796811
const ShadowNode& shadowNode) {
797812
if (!shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) {
@@ -813,7 +828,7 @@ void FabricMountingManager::maybePreallocateShadowNode(
813828
preallocatedViewsQueue_.push_back(std::move(shadowView));
814829
} else {
815830
// Old implementation where FabricUIManager.preallocateView is called
816-
// immediatelly.
831+
// immediately.
817832
preallocateShadowView(shadowView);
818833
}
819834
}

packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class FabricMountingManager final {
3434

3535
void maybePreallocateShadowNode(const ShadowNode& shadowNode);
3636

37+
void destroyUnmountedShadowNode(const ShadowNodeFamily& family);
38+
3739
/*
3840
* Drains preallocatedViewsQueue_ by calling preallocateShadowView on each
3941
* item in the queue. Can be called by any thread.

packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ void ShadowNode::cloneChildrenIfShared() {
282282
void ShadowNode::setMounted(bool mounted) const {
283283
if (mounted) {
284284
family_->setMostRecentState(getState());
285+
family_->setMounted();
285286
hasBeenMounted_ = mounted;
286287
}
287288

packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "ShadowNode.h"
1010

1111
#include <react/debug/react_native_assert.h>
12+
#include <react/featureflags/ReactNativeFeatureFlags.h>
1213
#include <react/renderer/core/ComponentDescriptor.h>
1314
#include <react/renderer/core/State.h>
1415

@@ -58,10 +59,30 @@ ComponentName ShadowNodeFamily::getComponentName() const {
5859
return componentName_;
5960
}
6061

62+
void ShadowNodeFamily::setMounted() const {
63+
hasBeenMounted_ = true;
64+
}
65+
6166
const ComponentDescriptor& ShadowNodeFamily::getComponentDescriptor() const {
6267
return componentDescriptor_;
6368
}
6469

70+
void ShadowNodeFamily::onUnmountedFamilyDestroyed(
71+
std::function<void(const ShadowNodeFamily& family)> callback) const {
72+
onUnmountedFamilyDestroyedCallback_ = std::move(callback);
73+
}
74+
75+
Tag ShadowNodeFamily::getTag() const {
76+
return tag_;
77+
}
78+
79+
ShadowNodeFamily::~ShadowNodeFamily() {
80+
if (ReactNativeFeatureFlags::enableDeletionOfUnmountedViews() &&
81+
!hasBeenMounted_ && onUnmountedFamilyDestroyedCallback_ != nullptr) {
82+
onUnmountedFamilyDestroyedCallback_(*this);
83+
}
84+
}
85+
6586
AncestorList ShadowNodeFamily::getAncestors(
6687
const ShadowNode& ancestorShadowNode) const {
6788
auto families = std::vector<const ShadowNodeFamily*>{};

packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class State;
2525
* `ShadowNodeFamily` instances.
2626
*
2727
* Do not use this class as a general purpose container to share information
28-
* about a `ShadowNodeFamily`. Pelase define specific purpose containers in
28+
* about a `ShadowNodeFamily`. Please define specific purpose containers in
2929
* those cases.
3030
*
3131
*/
@@ -87,12 +87,24 @@ class ShadowNodeFamily final {
8787

8888
SharedEventEmitter getEventEmitter() const;
8989

90+
/**
91+
* @param callback will be executed when an unmounted instance of
92+
* ShadowNodeFamily is destroyed.
93+
*/
94+
void onUnmountedFamilyDestroyed(
95+
std::function<void(const ShadowNodeFamily& family)> callback) const;
96+
9097
/*
9198
* Sets and gets the most recent state.
9299
*/
93100
std::shared_ptr<const State> getMostRecentState() const;
94101
void setMostRecentState(const std::shared_ptr<const State>& state) const;
95102

103+
/**
104+
* Mark this ShadowNodeFamily as mounted.
105+
*/
106+
void setMounted() const;
107+
96108
/*
97109
* Dispatches a state update with given priority.
98110
*/
@@ -105,6 +117,17 @@ class ShadowNodeFamily final {
105117
*/
106118
mutable std::unique_ptr<folly::dynamic> nativeProps_DEPRECATED;
107119

120+
/**
121+
* @return tag for the ShadowNodeFamily.
122+
*/
123+
Tag getTag() const;
124+
125+
/**
126+
* Override destructor to call onUnmountedFamilyDestroyedCallback() for
127+
* ShadowViews that were preallocated but never mounted on the screen.
128+
*/
129+
~ShadowNodeFamily();
130+
108131
private:
109132
friend ShadowNode;
110133
friend State;
@@ -121,6 +144,9 @@ class ShadowNodeFamily final {
121144
mutable std::shared_ptr<const State> mostRecentState_;
122145
mutable std::shared_mutex mutex_;
123146

147+
mutable std::function<void(ShadowNodeFamily& family)>
148+
onUnmountedFamilyDestroyedCallback_ = nullptr;
149+
124150
/*
125151
* Deprecated.
126152
*/
@@ -165,6 +191,11 @@ class ShadowNodeFamily final {
165191
* For optimization purposes only.
166192
*/
167193
mutable bool hasParent_{false};
194+
195+
/*
196+
* Determines if the ShadowNodeFamily was ever mounted on the screen.
197+
*/
198+
mutable bool hasBeenMounted_{false};
168199
};
169200

170201
} // namespace facebook::react

0 commit comments

Comments
 (0)