From 7d3de1016a37e28977840d71104b9f1348525419 Mon Sep 17 00:00:00 2001 From: David Vacca Date: Mon, 19 Mar 2018 18:21:39 -0700 Subject: [PATCH] Implement Fabric Reconciler Reviewed By: achen1 Differential Revision: D7240208 fbshipit-source-id: 236b76146c50fb7f357190b08f8a5bfcef7f6645 --- .../com/facebook/react/common/ArrayUtils.java | 9 + .../main/java/com/facebook/react/fabric/BUCK | 1 + .../react/fabric/FabricReconciler.java | 108 +++++++++++ .../react/fabric/FabricUIManager.java | 180 +++++++++--------- .../react/uimanager/ReactShadowNodeImpl.java | 8 + 5 files changed, 214 insertions(+), 92 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java b/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java index 0036e9bac13..41b62c19f25 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/ArrayUtils.java @@ -1,6 +1,7 @@ package com.facebook.react.common; import java.util.Arrays; +import java.util.List; public class ArrayUtils { @@ -8,4 +9,12 @@ public class ArrayUtils { return array == null ? null : Arrays.copyOf(array, array.length); } + public static int[] copyListToArray(List list) { + int[] array = new int[list.size()]; + for (int t = 0 ; t < list.size() ; t++) { + array[t] = list.get(t); + } + return array; + } + } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK index a5c3d74099b..da59ddbf2bf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK @@ -18,6 +18,7 @@ rn_android_library( react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), react_native_target("java/com/facebook/react/module/annotations:annotations"), + react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/modules/i18nmanager:i18nmanager"), ], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java new file mode 100644 index 00000000000..a4666759c99 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricReconciler.java @@ -0,0 +1,108 @@ +/** + * 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; + +import com.facebook.react.common.ArrayUtils; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.react.uimanager.UIViewOperationQueue; +import com.facebook.react.uimanager.ViewAtIndex; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +public class FabricReconciler { + + private UIViewOperationQueue uiViewOperationQueue; + + public FabricReconciler(UIViewOperationQueue uiViewOperationQueue) { + this.uiViewOperationQueue = uiViewOperationQueue; + } + + public void manageChildren(ReactShadowNode previousRootShadowNode, ReactShadowNode newRootShadowNode) { + List prevList = + previousRootShadowNode == null ? null : previousRootShadowNode.getChildrenList(); + manageChildren(newRootShadowNode, prevList, newRootShadowNode.getChildrenList()); + } + + private void manageChildren( + ReactShadowNode parent, + @Nullable List prevList, + @Nullable List newList) { + prevList = prevList == null ? Collections.emptyList() : prevList; + newList = newList == null ? Collections.emptyList() : newList; + + // Iterate through each child list and compare each previous and next child. Same nodes + // implies no change is needed. If the nodes are different but are referencing the same view, + // the view needs to be updated with new props and children. Otherwise, there has been + // a change in the children positions. + int sameReactTagIndex = 0; + for (; sameReactTagIndex < Math.min(prevList.size(), newList.size()); sameReactTagIndex++) { + ReactShadowNode prevNode = prevList.get(sameReactTagIndex); + ReactShadowNode newNode = newList.get(sameReactTagIndex); + if (prevNode == newNode) { + continue; + } + if (prevNode.getReactTag() != newNode.getReactTag()) { + break; + } + + if (newNode.getNewProps() != null) { + uiViewOperationQueue.enqueueUpdateProperties( + newNode.getReactTag(), newNode.getViewClass(), newNode.getNewProps()); + } + + manageChildren(prevNode, prevNode.getChildrenList(), newNode.getChildrenList()); + prevNode.setOriginalReactShadowNode(newNode); + } + int firstRemovedOrAddedViewIndex = sameReactTagIndex; + + // Every ReactShadowNode on the newList that is on the right side of + // firstRemovedOrAddedViewIndex is defined as an added view. + // It is more efficient to reorder removing and adding all the views in the right order, instead + // of calculating the minimum amount of reorder operations. + Set addedTags = new HashSet<>(); + ViewAtIndex[] viewsToAdd = new ViewAtIndex[newList.size() - firstRemovedOrAddedViewIndex]; + int viewsToAddIndex = 0; + for (int k = firstRemovedOrAddedViewIndex; k < newList.size(); k++) { + ReactShadowNode newNode = newList.get(k); + if (newNode.getNewProps() != null) { + uiViewOperationQueue.enqueueUpdateProperties( + newNode.getReactTag(), newNode.getViewClass(), newNode.getNewProps()); + } + viewsToAdd[viewsToAddIndex++] = new ViewAtIndex(newNode.getReactTag(), k); + List previousChildrenList = newNode.getOriginalReactShadowNode() == null ? null : newNode.getOriginalReactShadowNode().getChildrenList(); + manageChildren(newNode, previousChildrenList, newNode.getChildrenList()); + newNode.setOriginalReactShadowNode(newNode); + addedTags.add(newNode.getReactTag()); + } + + // Every ReactShadowNode on the prevList that is on the right side of + // firstRemovedOrAddedViewIndex is defined as a removed view. + // It is more efficient to reorder removing and adding all the views in the right order, instead + // of calculating the minimum amount of reorder operations. + // If a View is not re-ordered, then the ReactTag is deleted (ReactShadowNode and native View + // are released from memory) + List tagsToDelete = new LinkedList<>(); + int[] indicesToRemove = new int[prevList.size() - firstRemovedOrAddedViewIndex]; + int indicesToRemoveIndex = 0; + for (int j = firstRemovedOrAddedViewIndex; j < prevList.size(); j++) { + ReactShadowNode nodeToRemove = prevList.get(j); + indicesToRemove[indicesToRemoveIndex++] = j; + if (!addedTags.contains(nodeToRemove.getReactTag())) { + tagsToDelete.add(nodeToRemove.getReactTag()); + } + } + + uiViewOperationQueue.enqueueManageChildren( + parent.getReactTag(), indicesToRemove, viewsToAdd, ArrayUtils.copyListToArray(tagsToDelete)); + } + +} 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 f66b0ce5682..4820e8496d8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -14,7 +14,6 @@ import static android.view.View.MeasureSpec.UNSPECIFIED; import android.util.Log; import android.view.View; import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.JavaOnlyArray; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; @@ -28,7 +27,6 @@ import com.facebook.react.uimanager.ReactShadowNodeImpl; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIViewOperationQueue; -import com.facebook.react.uimanager.ViewAtIndex; import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManagerRegistry; import com.facebook.react.uimanager.common.MeasureSpecProvider; @@ -38,8 +36,8 @@ import java.util.List; import javax.annotation.Nullable; /** - * This class is responsible to create, clone and update {@link ReactShadowNode} using the - * Fabric API. + * This class is responsible to create, clone and update {@link ReactShadowNode} using the Fabric + * API. */ @SuppressWarnings("unused") // used from JNI public class FabricUIManager implements UIManager { @@ -49,23 +47,25 @@ public class FabricUIManager implements UIManager { private final ReactApplicationContext mReactApplicationContext; private final ViewManagerRegistry mViewManagerRegistry; private final UIViewOperationQueue mUIViewOperationQueue; + private volatile int mCurrentBatch = 0; + private ReactShadowNode mCurrentRootShadowNode; + private FabricReconciler mFabricReconciler; - public FabricUIManager(ReactApplicationContext reactContext, - ViewManagerRegistry viewManagerRegistry) { + public FabricUIManager( + ReactApplicationContext reactContext, ViewManagerRegistry viewManagerRegistry) { DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(reactContext); mReactApplicationContext = reactContext; mViewManagerRegistry = viewManagerRegistry; - mUIViewOperationQueue = new UIViewOperationQueue(reactContext, new NativeViewHierarchyManager(viewManagerRegistry), 0); + mUIViewOperationQueue = + new UIViewOperationQueue( + reactContext, new NativeViewHierarchyManager(viewManagerRegistry), 0); + mFabricReconciler = new FabricReconciler(mUIViewOperationQueue); } - /** - * Creates a new {@link ReactShadowNode} - */ + /** Creates a new {@link ReactShadowNode} */ @Nullable - public ReactShadowNode createNode(int reactTag, - String viewName, - int rootTag, - ReadableNativeMap props) { + public ReactShadowNode createNode( + int reactTag, String viewName, int rootTag, ReadableNativeMap props) { try { ViewManager viewManager = mViewManagerRegistry.get(viewName); ReactShadowNode node = viewManager.createShadowNodeInstance(mReactApplicationContext); @@ -78,8 +78,8 @@ public class FabricUIManager implements UIManager { ReactStylesDiffMap styles = updateProps(node, props); if (!node.isVirtual()) { - mUIViewOperationQueue - .enqueueCreateView(rootNode.getThemedContext(), reactTag, viewName, styles); + mUIViewOperationQueue.enqueueCreateView( + rootNode.getThemedContext(), reactTag, viewName, styles); } return node; } catch (Throwable t) { @@ -103,8 +103,8 @@ public class FabricUIManager implements UIManager { /** * @return a clone of the {@link ReactShadowNode} received by parameter. The cloned - * ReactShadowNode will contain a copy of all the internal data of the original node, including - * its children set (note that the children nodes will not be cloned). + * ReactShadowNode will contain a copy of all the internal data of the original node, + * including its children set (note that the children nodes will not be cloned). */ @Nullable public ReactShadowNode cloneNode(ReactShadowNode node) { @@ -120,8 +120,8 @@ public class FabricUIManager implements UIManager { /** * @return a clone of the {@link ReactShadowNode} received by parameter. The cloned - * ReactShadowNode will contain a copy of all the internal data of the original node, but - * its children set will be empty. + * ReactShadowNode will contain a copy of all the internal data of the original node, but its + * children set will be empty. */ @Nullable public ReactShadowNode cloneNodeWithNewChildren(ReactShadowNode node) { @@ -137,15 +137,15 @@ public class FabricUIManager implements UIManager { /** * @return a clone of the {@link ReactShadowNode} received by parameter. The cloned - * ReactShadowNode will contain a copy of all the internal data of the original node, but its - * props will be overridden with the {@link ReadableMap} received by parameter. + * ReactShadowNode will contain a copy of all the internal data of the original node, but its + * props will be overridden with the {@link ReadableMap} received by parameter. */ @Nullable public ReactShadowNode cloneNodeWithNewProps( - ReactShadowNode node, - @Nullable ReadableNativeMap newProps) { + ReactShadowNode node, @Nullable ReadableNativeMap newProps) { try { - ReactShadowNode clone = node.mutableCopyWithNewProps(newProps == null ? null : new ReactStylesDiffMap(newProps)); + ReactShadowNode clone = + node.mutableCopyWithNewProps(newProps == null ? null : new ReactStylesDiffMap(newProps)); assertReactShadowNodeCopy(node, clone); return clone; } catch (Throwable t) { @@ -156,16 +156,17 @@ public class FabricUIManager implements UIManager { /** * @return a clone of the {@link ReactShadowNode} received by parameter. The cloned - * ReactShadowNode will contain a copy of all the internal data of the original node, but its - * props will be overridden with the {@link ReadableMap} received by parameter and its children - * set will be empty. + * ReactShadowNode will contain a copy of all the internal data of the original node, but its + * props will be overridden with the {@link ReadableMap} received by parameter and its + * children set will be empty. */ @Nullable public ReactShadowNode cloneNodeWithNewChildrenAndProps( - ReactShadowNode node, - ReadableNativeMap newProps) { + ReactShadowNode node, ReadableNativeMap newProps) { try { - ReactShadowNode clone = node.mutableCopyWithNewChildrenAndProps(newProps == null ? null : new ReactStylesDiffMap(newProps)); + ReactShadowNode clone = + node.mutableCopyWithNewChildrenAndProps( + newProps == null ? null : new ReactStylesDiffMap(newProps)); assertReactShadowNodeCopy(node, clone); return clone; } catch (Throwable t) { @@ -175,38 +176,33 @@ public class FabricUIManager implements UIManager { } private void assertReactShadowNodeCopy(ReactShadowNode source, ReactShadowNode target) { - Assertions.assertCondition(source.getClass().equals(target.getClass()), - "Found " + target.getClass() + " class when expecting: " + source.getClass() + - ". Check that " + source.getClass() + " implements the copy() method correctly."); + Assertions.assertCondition( + source.getClass().equals(target.getClass()), + "Found " + + target.getClass() + + " class when expecting: " + + source.getClass() + + ". Check that " + + source.getClass() + + " implements the copy() method correctly."); } /** - * Appends the child {@link ReactShadowNode} to the children set of the parent - * {@link ReactShadowNode}. + * Appends the child {@link ReactShadowNode} to the children set of the parent {@link + * ReactShadowNode}. */ @Nullable public void appendChild(ReactShadowNode parent, ReactShadowNode child) { try { - int childIndex = parent.getChildCount(); - parent.addChildAt(child, childIndex); - ViewAtIndex[] viewsToAdd = - new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), childIndex)}; - if (!child.isVirtual()) { - mUIViewOperationQueue.enqueueManageChildren( - parent.getReactTag(), - null, - viewsToAdd, - null - ); - } + parent.addChildAt(child, parent.getChildCount()); } catch (Throwable t) { handleException(parent, t); } } /** - * @return an empty {@link List} that will be used to append the - * {@link ReactShadowNode} elements of the root. Typically this List will contain one element. + * @return an empty {@link List} that will be used to append the {@link + * ReactShadowNode} elements of the root. Typically this List will contain one element. */ public List createChildSet(int rootTag) { return new ArrayList<>(1); @@ -219,22 +215,24 @@ public class FabricUIManager implements UIManager { childList.add(child); } - public void completeRoot(int rootTag, List childList) { + public synchronized void completeRoot(int rootTag, List childList) { try { ReactShadowNode rootNode = getRootNode(rootTag); Assertions.assertNotNull( - rootNode, - "Root view with tag " + rootTag + " must be added before completeRoot is called"); - for (int i = 0; i < childList.size(); i++) { - ReactShadowNode child = childList.get(i); - appendChild(rootNode, child); - } + rootNode, + "Root view with tag " + rootTag + " must be added before completeRoot is called"); + + + rootNode = calculateDiffingAndCreateNewRootNode(rootNode, childList); notifyOnBeforeLayoutRecursive(rootNode); - calculateRootLayout(rootNode); + rootNode.calculateLayout(); + applyUpdatesRecursive(rootNode, 0, 0); - mUIViewOperationQueue - .dispatchViewUpdates(1, System.currentTimeMillis(), System.currentTimeMillis()); + mUIViewOperationQueue.dispatchViewUpdates( + mCurrentBatch++, System.currentTimeMillis(), System.currentTimeMillis()); + + mCurrentRootShadowNode = rootNode; } catch (Exception e) { handleException(getRootNode(rootTag), e); } @@ -250,46 +248,45 @@ public class FabricUIManager implements UIManager { node.onBeforeLayout(); } - private void calculateRootLayout(ReactShadowNode cssRoot) { - cssRoot.calculateLayout(); + private ReactShadowNode calculateDiffingAndCreateNewRootNode( + ReactShadowNode currentRootShadowNode, List newChildList) { + ReactShadowNode newRootShadowNode = currentRootShadowNode.mutableCopyWithNewChildren(); + for (ReactShadowNode child : newChildList) { + appendChild(newRootShadowNode, child); + } + + mFabricReconciler.manageChildren(mCurrentRootShadowNode, newRootShadowNode); + return newRootShadowNode; } - private void applyUpdatesRecursive( - ReactShadowNode cssNode, - float absoluteX, - float absoluteY) { - - if (!cssNode.hasUpdates()) { + private void applyUpdatesRecursive(ReactShadowNode node, float absoluteX, float absoluteY) { + if (!node.hasUpdates()) { return; } - if (!cssNode.isVirtualAnchor()) { - for (int i = 0; i < cssNode.getChildCount(); i++) { + if (!node.isVirtualAnchor()) { + for (int i = 0; i < node.getChildCount(); i++) { applyUpdatesRecursive( - cssNode.getChildAt(i), - absoluteX + cssNode.getLayoutX(), - absoluteY + cssNode.getLayoutY()); + node.getChildAt(i), + absoluteX + node.getLayoutX(), + absoluteY + node.getLayoutY()); } } - int tag = cssNode.getReactTag(); + int tag = node.getReactTag(); if (mRootShadowNodeRegistry.getNode(tag) == null) { - boolean frameDidChange = cssNode.dispatchUpdates( - absoluteX, - absoluteY, - mUIViewOperationQueue, - null); + boolean frameDidChange = + node.dispatchUpdates(absoluteX, absoluteY, mUIViewOperationQueue, null); } - cssNode.markUpdateSeen(); + node.markUpdateSeen(); } @Override public int addRootView( - final T rootView) { + final T rootView) { int rootTag = ReactRootViewTagGenerator.getNextRootViewTag(); - ThemedReactContext themedRootContext = new ThemedReactContext( - mReactApplicationContext, - rootView.getContext()); + ThemedReactContext themedRootContext = + new ThemedReactContext(mReactApplicationContext, rootView.getContext()); ReactShadowNode rootShadowNode = createRootShadowNode(rootTag, themedRootContext); @@ -321,18 +318,18 @@ public class FabricUIManager implements UIManager { * parameters. */ public void updateRootView( - ReactShadowNode rootCSSNode, int widthMeasureSpec, int heightMeasureSpec) { + ReactShadowNode node, int widthMeasureSpec, int heightMeasureSpec) { int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); int widthSize = View.MeasureSpec.getSize(widthMeasureSpec); switch (widthMode) { case EXACTLY: - rootCSSNode.setStyleWidth(widthSize); + node.setStyleWidth(widthSize); break; case AT_MOST: - rootCSSNode.setStyleMaxWidth(widthSize); + node.setStyleMaxWidth(widthSize); break; case UNSPECIFIED: - rootCSSNode.setStyleWidthAuto(); + node.setStyleWidthAuto(); break; } @@ -340,13 +337,13 @@ public class FabricUIManager implements UIManager { int heightSize = View.MeasureSpec.getSize(heightMeasureSpec); switch (heightMode) { case EXACTLY: - rootCSSNode.setStyleHeight(heightSize); + node.setStyleHeight(heightSize); break; case AT_MOST: - rootCSSNode.setStyleMaxHeight(heightSize); + node.setStyleMaxHeight(heightSize); break; case UNSPECIFIED: - rootCSSNode.setStyleHeightAuto(); + node.setStyleHeightAuto(); break; } } @@ -362,5 +359,4 @@ public class FabricUIManager implements UIManager { throw new RuntimeException(ex.getMessage(), t); } } - } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java index d097842e4e1..a43355e84fc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java @@ -132,6 +132,7 @@ public class ReactShadowNodeImpl implements ReactShadowNode arraycopy(original.mPaddingIsPercent, 0, mPaddingIsPercent, 0, original.mPaddingIsPercent.length); mNewProps = null; mParent = null; + mOriginalReactShadowNode = original; } /** @@ -151,6 +152,13 @@ public class ReactShadowNodeImpl implements ReactShadowNode copy.mTotalNativeChildren = mTotalNativeChildren; copy.mChildren = mChildren == null ? null : new ArrayList<>(mChildren); copy.mYogaNode.setData(this); + if (mChildren != null) { + for (ReactShadowNode child : mChildren) { + if (child.getOriginalReactShadowNode() == null) { + child.setOriginalReactShadowNode(child); + } + } + } return copy; }