Skip to content

Commit

Permalink
Delete pre-allocated views that were never mounted on the screen (#46473
Browse files Browse the repository at this point in the history
)

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
  • Loading branch information
mdvacca authored and facebook-github-bot committed Sep 16, 2024
1 parent 68a92aa commit 6dbbbe3
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(jint, jint)>(
"destroyUnmountedView");
destroyUnmountedView(javaUIManager_, surfaceId, tag);
});
}

void FabricMountingManager::maybePreallocateShadowNode(
const ShadowNode& shadowNode) {
if (!shadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView)) {
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ void ShadowNode::cloneChildrenIfShared() {
void ShadowNode::setMounted(bool mounted) const {
if (mounted) {
family_->setMostRecentState(getState());
family_->setMounted();
hasBeenMounted_ = mounted;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "ShadowNode.h"

#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/core/ComponentDescriptor.h>
#include <react/renderer/core/State.h>

Expand Down Expand Up @@ -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<void(const ShadowNodeFamily& family)> 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<const ShadowNodeFamily*>{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
*/
Expand Down Expand Up @@ -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<void(const ShadowNodeFamily& family)> callback) const;

/*
* Sets and gets the most recent state.
*/
std::shared_ptr<const State> getMostRecentState() const;
void setMostRecentState(const std::shared_ptr<const State>& state) const;

/**
* Mark this ShadowNodeFamily as mounted.
*/
void setMounted() const;

/*
* Dispatches a state update with given priority.
*/
Expand All @@ -105,6 +117,17 @@ class ShadowNodeFamily final {
*/
mutable std::unique_ptr<folly::dynamic> 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;
Expand All @@ -121,6 +144,9 @@ class ShadowNodeFamily final {
mutable std::shared_ptr<const State> mostRecentState_;
mutable std::shared_mutex mutex_;

mutable std::function<void(ShadowNodeFamily& family)>
onUnmountedFamilyDestroyedCallback_ = nullptr;

/*
* Deprecated.
*/
Expand Down Expand Up @@ -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

0 comments on commit 6dbbbe3

Please sign in to comment.