diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 0a8bb02c6ff..a3b69c3584c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -50,6 +50,7 @@ import com.facebook.react.fabric.mounting.mountitems.DispatchStringCommandMountI import com.facebook.react.fabric.mounting.mountitems.InsertMountItem; import com.facebook.react.fabric.mounting.mountitems.MountItem; import com.facebook.react.fabric.mounting.mountitems.PreAllocateViewMountItem; +import com.facebook.react.fabric.mounting.mountitems.RemoveDeleteMultiMountItem; import com.facebook.react.fabric.mounting.mountitems.RemoveMountItem; import com.facebook.react.fabric.mounting.mountitems.SendAccessibilityEvent; import com.facebook.react.fabric.mounting.mountitems.UpdateEventEmitterMountItem; @@ -280,6 +281,12 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { return new DeleteMountItem(reactTag); } + @DoNotStrip + @SuppressWarnings("unused") + private MountItem removeDeleteMultiMountItem(int[] metadata) { + return new RemoveDeleteMultiMountItem(metadata); + } + @DoNotStrip @SuppressWarnings("unused") private MountItem updateLayoutMountItem( diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index 6d693d76185..ae5a9b3fa36 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -42,6 +42,14 @@ struct JMountItem : public JavaClass { static constexpr auto UIManagerJavaDescriptor = "com/facebook/react/fabric/FabricUIManager"; +struct RemoveDeleteMetadata { + int tag; + int parentTag; + int index; + bool shouldRemove; + bool shouldDelete; +}; + } // namespace jni::local_ref Binding::initHybrid( @@ -223,7 +231,9 @@ void Binding::installFabricUIManager( contextContainer->insert("ReactNativeConfig", config); contextContainer->insert("FabricUIManager", javaUIManager_); + // Keep reference to config object and cache some feature flags here reactNativeConfig_ = config; + shouldCollateRemovesAndDeletes_ = reactNativeConfig_->getBool("react_fabric:enable_removedelete_collation_android"); auto toolbox = SchedulerToolbox{}; toolbox.contextContainer = contextContainer; @@ -411,9 +421,9 @@ local_ref createUpdateStateMountItem( const jni::global_ref &javaUIManager, const ShadowViewMutation &mutation) { static auto updateStateInstruction = - jni::findClassStatic(UIManagerJavaDescriptor) - ->getMethod(jint, jobject)>( - "updateStateMountItem"); + jni::findClassStatic(UIManagerJavaDescriptor) + ->getMethod(jint, jobject)>( + "updateStateMountItem"); auto state = mutation.newChildShadowView.state; @@ -428,9 +438,9 @@ local_ref createUpdateStateMountItem( } return updateStateInstruction( - javaUIManager, - mutation.newChildShadowView.tag, - (javaStateWrapper != nullptr ? javaStateWrapper.get() : nullptr)); + javaUIManager, + mutation.newChildShadowView.tag, + (javaStateWrapper != nullptr ? javaStateWrapper.get() : nullptr)); } local_ref createRemoveMountItem( @@ -458,6 +468,30 @@ local_ref createDeleteMountItem( return deleteInstruction(javaUIManager, mutation.oldChildShadowView.tag); } +local_ref createRemoveAndDeleteMultiMountItem( + const jni::global_ref &javaUIManager, + std::vector metadata) { + + auto env = Environment::current(); + auto removeAndDeleteArray = env->NewIntArray(metadata.size()*4); + int position = 0; + jint temp[4]; + for (const auto& x : metadata) { + temp[0] = x.tag; + temp[1] = x.parentTag; + temp[2] = x.index; + temp[3] = (x.shouldRemove ? 1 : 0) | (x.shouldDelete ? 2 : 0); + env->SetIntArrayRegion(removeAndDeleteArray, position, 4, temp); + position += 4; + } + + static auto removeDeleteMultiInstruction = + jni::findClassStatic(UIManagerJavaDescriptor) + ->getMethod(jintArray)>("removeDeleteMultiMountItem"); + + return removeDeleteMultiInstruction(javaUIManager, removeAndDeleteArray); +} + // TODO T48019320: because we pass initial props and state to the Create (and preallocate) mount instruction, // we technically don't need to pass the first Update to any components. Dedupe? local_ref createCreateMountItem( @@ -535,6 +569,9 @@ void Binding::schedulerDidFinishTransaction( auto mountItems = *(mountItemsArray); std::unordered_set deletedViewTags; + // Find the set of tags that are removed and deleted in one block + std::vector toRemove; + int position = 0; for (const auto &mutation : mutations) { auto oldChildShadowView = mutation.oldChildShadowView; @@ -543,6 +580,14 @@ void Binding::schedulerDidFinishTransaction( bool isVirtual = newChildShadowView.layoutMetrics == EmptyLayoutMetrics && oldChildShadowView.layoutMetrics == EmptyLayoutMetrics; + // Handle accumulated removals/deletions + if (shouldCollateRemovesAndDeletes_ && mutation.type != ShadowViewMutation::Remove && mutation.type != ShadowViewMutation::Delete) { + if (toRemove.size() > 0) { + mountItems[position++] = createRemoveAndDeleteMultiMountItem(localJavaUIManager, toRemove); + toRemove.clear(); + } + } + switch (mutation.type) { case ShadowViewMutation::Create: { if (mutation.newChildShadowView.props->revision > 1 || @@ -555,14 +600,27 @@ void Binding::schedulerDidFinishTransaction( } case ShadowViewMutation::Remove: { if (!isVirtual) { - mountItems[position++] = - createRemoveMountItem(localJavaUIManager, mutation); + if (shouldCollateRemovesAndDeletes_) { + toRemove.push_back(RemoveDeleteMetadata{mutation.oldChildShadowView.tag, mutation.parentShadowView.tag, mutation.index, true, false}); + } else { + mountItems[position++] = createRemoveMountItem(localJavaUIManager, mutation); + } } break; } case ShadowViewMutation::Delete: { - mountItems[position++] = - createDeleteMountItem(localJavaUIManager, mutation); + if (shouldCollateRemovesAndDeletes_) { + // It is impossible to delete without removing node first + const auto& it = std::find_if(std::begin(toRemove), std::end(toRemove), [&mutation](const auto& x) { return x.tag == mutation.oldChildShadowView.tag; }); + + if (it != std::end(toRemove)) { + it->shouldDelete = true; + } else { + toRemove.push_back(RemoveDeleteMetadata{mutation.oldChildShadowView.tag, -1, -1, false, true}); + } + } else { + mountItems[position++] = createDeleteMountItem(localJavaUIManager, mutation); + } deletedViewTags.insert(mutation.oldChildShadowView.tag); break; @@ -662,6 +720,12 @@ void Binding::schedulerDidFinishTransaction( } } + // Handle remaining removals and deletions + if (shouldCollateRemovesAndDeletes_ && toRemove.size() > 0) { + mountItems[position++] = createRemoveAndDeleteMultiMountItem(localJavaUIManager, toRemove); + toRemove.clear(); + } + if (position <= 0) { // If there are no mountItems to be sent to the platform, then it is not necessary to even call. return; diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h index 1f9f448fa47..5428be349ea 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.h @@ -100,7 +100,8 @@ class Binding : public jni::HybridClass, public SchedulerDelegate { float pointScaleFactor_ = 1; - std::shared_ptr reactNativeConfig_; + std::shared_ptr reactNativeConfig_{nullptr}; + bool shouldCollateRemovesAndDeletes_{false}; }; } // namespace react diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java new file mode 100644 index 00000000000..13cb28fa4a1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/RemoveDeleteMultiMountItem.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + *

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; + +public class RemoveDeleteMultiMountItem implements MountItem { + + // Metadata is an array of ints, grouped into 4 ints per instruction (so the length of metadata + // is always divisible by 4): + // + // `instruction*4 + 0`: react tag of view instruction + // `instruction*4 + 1`: react tag of view's parent + // `instruction*4 + 2`: index of view in parents' children instruction + // `instruction*4 + 3`: flags indicating if the view should be removed, and/or deleted + private int[] mMetadata; + + // Bitfields of "flag", indicating if a view should be removed and/or deleted + private static final int REMOVE_FLAG = 1; + private static final int DELETE_FLAG = 2; + + // Indices for each parameter within an "instruction" + private static final int INSTRUCTION_FIELDS_LEN = 4; + private static final int TAG_INDEX = 0; + private static final int PARENT_TAG_INDEX = 1; + private static final int VIEW_INDEX_INDEX = 2; + private static final int FLAGS_INDEX = 3; + + public RemoveDeleteMultiMountItem(int[] metadata) { + mMetadata = metadata; + } + + @Override + public void execute(MountingManager mountingManager) { + // First, go through instructions and remove all views that are marked + // for removal. + // Not all views that are removed are deleted, and not all deleted views + // are removed first. + // *All* views must be removed here before we can delete any views. + // Removal of a view from a parent is based on indices within the parents' children, + // and deletion causes reordering; so we must perform all removals first. + for (int i = 0; i < mMetadata.length; i += INSTRUCTION_FIELDS_LEN) { + int flags = mMetadata[i + FLAGS_INDEX]; + if ((flags & REMOVE_FLAG) != 0) { + int parentTag = mMetadata[i + PARENT_TAG_INDEX]; + int index = mMetadata[i + VIEW_INDEX_INDEX]; + mountingManager.removeViewAt(parentTag, index); + } + } + + // After removing all views, delete all views marked for deletion. + for (int i = 0; i < mMetadata.length; i += 4) { + int flags = mMetadata[i + FLAGS_INDEX]; + if ((flags & DELETE_FLAG) != 0) { + int tag = mMetadata[i + TAG_INDEX]; + mountingManager.deleteView(tag); + } + } + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + for (int i = 0; i < mMetadata.length; i += 4) { + if (s.length() > 0) { + s.append("\n"); + } + s.append("RemoveDeleteMultiMountItem (") + .append(i / INSTRUCTION_FIELDS_LEN + 1) + .append("/") + .append(mMetadata.length / INSTRUCTION_FIELDS_LEN) + .append("): [") + .append(mMetadata[i + TAG_INDEX]) + .append("] parent [") + .append(mMetadata[i + PARENT_TAG_INDEX]) + .append("] idx ") + .append(mMetadata[i + VIEW_INDEX_INDEX]) + .append(" ") + .append(mMetadata[i + FLAGS_INDEX]); + } + return s.toString(); + } +}