From 7b778fbebb9e11896f0ef1578645232bb9bf0c7f Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 26 May 2022 03:40:21 -0700 Subject: [PATCH] Prototype View recycling for View Summary: Prototype of View Recycling for View + generic APIs. Changelog: [Added][Android] Adding experimental View Recycling for Fabric on Android. Reviewed By: mdvacca Differential Revision: D36608419 fbshipit-source-id: c469ce2fe12ef9332d3def591118befc4a619870 --- .../react/config/ReactFeatureFlags.java | 5 + .../mounting/SurfaceMountingManager.java | 4 + .../react/uimanager/BaseViewManager.java | 42 +++++++++ .../facebook/react/uimanager/ViewManager.java | 77 ++++++++++++++- .../react/uimanager/ViewManagerRegistry.java | 56 +++++++++++ .../java/com/facebook/react/views/view/BUCK | 1 + .../view/ReactViewBackgroundManager.java | 6 ++ .../react/views/view/ReactViewGroup.java | 94 +++++++++++++++++-- .../react/views/view/ReactViewManager.java | 19 ++++ 9 files changed, 293 insertions(+), 11 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 6816b08ebe2..a57476df202 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -119,4 +119,9 @@ public class ReactFeatureFlags { * */ public static int turboModuleBindingMode = 0; + + /** + * Feature Flag to enable View Recycling. When enabled, individual ViewManagers must still opt-in. + */ + public static boolean enableViewRecycling = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java index 176ac68cba5..6a94f1e8b0e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java @@ -304,6 +304,10 @@ public class SurfaceMountingManager { mRootViewManager = null; mMountItemExecutor = null; mOnViewAttachItems.clear(); + + if (ReactFeatureFlags.enableViewRecycling) { + mViewManagerRegistry.onSurfaceStopped(mSurfaceId); + } } }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index a877786acb8..1e7bc2b90a2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -64,6 +64,48 @@ public abstract class BaseViewManager extends BaseJavaModule { + /** + * For View recycling: we store a Stack of unused, dead Views. This is null by default, and when + * null signals that View Recycling is disabled. `enableViewRecycling` must be explicitly called + * in a concrete constructor to enable View Recycling per ViewManager. + */ + private HashMap> mRecyclableViews = null; + + /** Call in constructor of concrete ViewManager class to enable. */ + protected void enableViewRecycling() { + if (ReactFeatureFlags.enableViewRecycling) { + mRecyclableViews = new HashMap<>(); + } + } + + private @Nullable Stack getRecyclableViewStack(int surfaceId) { + if (mRecyclableViews == null) { + return null; + } + if (!mRecyclableViews.containsKey(surfaceId)) { + mRecyclableViews.put(surfaceId, new Stack<>()); + } + return mRecyclableViews.get(surfaceId); + } + /** * For the vast majority of ViewManagers, you will not need to override this. Only override this * if you really know what you're doing and have a very unique use-case. @@ -137,7 +164,13 @@ public abstract class ViewManager @NonNull ThemedReactContext reactContext, @Nullable ReactStylesDiffMap initialProps, @Nullable StateWrapper stateWrapper) { - T view = createViewInstance(reactContext); + T view = null; + @Nullable Stack recyclableViews = getRecyclableViewStack(reactContext.getSurfaceId()); + if (recyclableViews != null && !recyclableViews.empty()) { + view = recycleView(reactContext, recyclableViews.pop()); + } else { + view = createViewInstance(reactContext); + } view.setId(reactTag); addEventEmitters(reactContext, view); if (initialProps != null) { @@ -157,7 +190,27 @@ public abstract class ViewManager * Called when view is detached from view hierarchy and allows for some additional cleanup by the * {@link ViewManager} subclass. */ - public void onDropViewInstance(@NonNull T view) {} + public void onDropViewInstance(@NonNull T view) { + @Nullable + Stack recyclableViews = + getRecyclableViewStack(((ThemedReactContext) view.getContext()).getSurfaceId()); + // By default we treat views as recyclable + if (recyclableViews != null) { + recyclableViews.push(prepareToRecycleView((ThemedReactContext) view.getContext(), view)); + } + } + + /** + * Called when a View is removed from the hierachy. This should be used to reset any properties. + */ + protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, @NonNull T view) { + return view; + } + + /** Called when a View is going to be reused. */ + protected T recycleView(@NonNull ThemedReactContext reactContext, @NonNull T view) { + return view; + } /** * Subclasses can override this method to install custom event emitters on the given View. You @@ -370,4 +423,24 @@ public abstract class ViewManager * components support setting padding, the default implementation of this method does nothing. */ public void setPadding(T view, int left, int top, int right, int bottom) {} + + /** + * Lifecycle method: called when a surface is stopped. Currently only used for View Recycling + * cleanup. There is no corresponding startSurface lifecycle event for ViewManagers because we + * currently only need this for recycling cleanup. Only called in Fabric. + */ + public void onSurfaceStopped(int surfaceId) { + if (mRecyclableViews != null) { + mRecyclableViews.remove(surfaceId); + } + } + + /** With even slight memory pressure, we immediately evict all recyclable Views. */ + /* package */ void trimMemory() { + // Wipe out all existing recyclable Views, but do not disable View Recycling entirely. + // We only take any action if View Recycling is already enabled. + if (mRecyclableViews != null) { + mRecyclableViews = new HashMap<>(); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java index 54c7d78d8cf..fea9babb240 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerRegistry.java @@ -7,7 +7,10 @@ package com.facebook.react.uimanager; +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; import androidx.annotation.Nullable; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.MapBuilder; import java.util.List; import java.util.Map; @@ -20,6 +23,7 @@ public final class ViewManagerRegistry { private final Map mViewManagers; private final @Nullable ViewManagerResolver mViewManagerResolver; + private final MemoryTrimCallback mMemoryTrimCallback = new MemoryTrimCallback(); public ViewManagerRegistry(ViewManagerResolver viewManagerResolver) { mViewManagers = MapBuilder.newHashMap(); @@ -42,6 +46,40 @@ public final class ViewManagerRegistry { mViewManagerResolver = null; } + /** + * Trim View Recycling memory aggressively. Whenever the system is running even slightly low on + * memory or is backgrounded, we immediately flush all recyclable Views. GC and memory swaps cause + * intense CPU pressure, so we always favor low memory usage over View recycling, even if there is + * only "moderate" pressure. + */ + private class MemoryTrimCallback implements ComponentCallbacks2 { + @Override + public void onTrimMemory(int level) { + Runnable runnable = + new Runnable() { + @Override + public void run() { + for (Map.Entry entry : mViewManagers.entrySet()) { + entry.getValue().trimMemory(); + } + } + }; + if (UiThreadUtil.isOnUiThread()) { + runnable.run(); + } else { + UiThreadUtil.runOnUiThread(runnable); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) {} + + @Override + public void onLowMemory() { + this.onTrimMemory(0); + } + } + /** * @param className {@link String} that identifies the {@link ViewManager} inside the {@link * ViewManagerRegistry}. This methods {@throws IllegalViewOperationException} if there is no @@ -91,4 +129,22 @@ public final class ViewManagerRegistry { } return null; } + + /** Send lifecycle signal to all ViewManagers that StopSurface has been called. */ + public void onSurfaceStopped(int surfaceId) { + Runnable runnable = + new Runnable() { + @Override + public void run() { + for (Map.Entry entry : mViewManagers.entrySet()) { + entry.getValue().onSurfaceStopped(surfaceId); + } + } + }; + if (UiThreadUtil.isOnUiThread()) { + runnable.run(); + } else { + UiThreadUtil.runOnUiThread(runnable); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK index be7b9f9e6f2..4c78e7313b1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/BUCK @@ -39,5 +39,6 @@ rn_android_library( react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/modules/i18nmanager:i18nmanager"), react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), + react_native_target("res:uimanager"), ], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java index 19bf56f381b..4a5fce52c3a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java @@ -24,6 +24,12 @@ public class ReactViewBackgroundManager { this.mView = view; } + public void cleanup() { + ViewCompat.setBackground(mView, null); + this.mView = null; + this.mReactBackgroundDrawable = null; + } + private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { if (mReactBackgroundDrawable == null) { mReactBackgroundDrawable = new ReactViewBackgroundDrawable(mView.getContext()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index a87815b2d8d..e0ca386f36f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -116,26 +116,93 @@ public class ReactViewGroup extends ViewGroup // whenever the option is set. We also override {@link ViewGroup#getChildAt} and // {@link ViewGroup#getChildCount} so those methods may return views that are not attached. // This is risky but allows us to perform a correct cleanup in {@link NativeViewHierarchyManager}. - private boolean mRemoveClippedSubviews = false; - private @Nullable View[] mAllChildren = null; + private boolean mRemoveClippedSubviews; + private @Nullable View[] mAllChildren; private int mAllChildrenCount; private @Nullable Rect mClippingRect; private @Nullable Rect mHitSlopRect; private @Nullable String mOverflow; - private PointerEvents mPointerEvents = PointerEvents.AUTO; + private PointerEvents mPointerEvents; private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener; private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable; private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener; - private boolean mNeedsOffscreenAlphaCompositing = false; - private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper = null; + private boolean mNeedsOffscreenAlphaCompositing; + private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper; private @Nullable Path mPath; private int mLayoutDirection; - private float mBackfaceOpacity = 1.f; - private String mBackfaceVisibility = "visible"; + private float mBackfaceOpacity; + private String mBackfaceVisibility; public ReactViewGroup(Context context) { super(context); + initView(); + } + + /** + * Set all default values here as opposed to in the constructor or field defaults. It is important + * that these properties are set during the constructor, but also on-demand whenever an existing + * ReactTextView is recycled. + */ + private void initView() { setClipChildren(false); + + mRemoveClippedSubviews = false; + mAllChildren = null; + mAllChildrenCount = 0; + mClippingRect = null; + mHitSlopRect = null; + mOverflow = null; + mPointerEvents = PointerEvents.AUTO; + mChildrenLayoutChangeListener = null; + mReactBackgroundDrawable = null; + mOnInterceptTouchEventListener = null; + mNeedsOffscreenAlphaCompositing = false; + mDrawingOrderHelper = null; + mPath = null; + mLayoutDirection = 0; // set when background is created + mBackfaceOpacity = 1.f; + mBackfaceVisibility = "visible"; + } + + /* package */ void recycleView() { + // Set default field values + initView(); + mOverflowInset.setEmpty(); + sHelperRect.setEmpty(); + + // Remove any children + removeAllViews(); + + // Reset background, borders + updateBackgroundDrawable(null); + + setForeground(null); + + // This is possibly subject to change and overrideable per-platform, but these + // are the default view flags in View.java: + // https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#2712 + // `mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | LAYOUT_DIRECTION_INHERIT` + // Therefore we set the following options as such: + setFocusable(false); + setFocusableInTouchMode(false); + + // Focus IDs + // Also see in AOSP source: + // https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#4493 + setNextFocusDownId(View.NO_ID); + setNextFocusForwardId(View.NO_ID); + setNextFocusRightId(View.NO_ID); + setNextFocusUpId(View.NO_ID); + + // Predictable, alpha defaults to 1: + // https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#2186 + // This accounts for resetting mBackfaceOpacity and mBackfaceVisibility + setAlpha(1); + + // https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-mainline-12.0.0_r96/core/java/android/view/View.java#5491 + setElevation(0); + + resetPointerEvents(); } private ViewGroupDrawingOrderHelper getDrawingOrderHelper() { @@ -553,6 +620,10 @@ public class ReactViewGroup extends ViewGroup mPointerEvents = pointerEvents; } + /*package*/ void resetPointerEvents() { + mPointerEvents = PointerEvents.AUTO; + } + /*package*/ int getAllChildrenCount() { return mAllChildrenCount; } @@ -696,7 +767,7 @@ public class ReactViewGroup extends ViewGroup return DEFAULT_BACKGROUND_COLOR; } - private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { + /* package */ ReactViewBackgroundDrawable getOrCreateReactViewBackground() { if (mReactBackgroundDrawable == null) { mReactBackgroundDrawable = new ReactViewBackgroundDrawable(getContext()); Drawable backgroundDrawable = getBackground(); @@ -742,6 +813,11 @@ public class ReactViewGroup extends ViewGroup mOverflowInset.set(left, top, right, bottom); } + public void setOverflowInset(Rect overflowInset) { + mOverflowInset.set( + overflowInset.left, overflowInset.top, overflowInset.right, overflowInset.bottom); + } + @Override public Rect getOverflowInset() { return mOverflowInset; @@ -754,7 +830,7 @@ public class ReactViewGroup extends ViewGroup * @param drawable {@link Drawable} The Drawable to use as the background, or null to remove the * background */ - private void updateBackgroundDrawable(Drawable drawable) { + /* package */ void updateBackgroundDrawable(Drawable drawable) { super.setBackground(drawable); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 8ec0f5660ae..59098e792bc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -52,6 +52,25 @@ public class ReactViewManager extends ReactClippingViewManager { private static final int CMD_SET_PRESSED = 2; private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate"; + private ReactViewGroup mDefaultViewForRecycling = null; + + public ReactViewManager() { + super(); + + enableViewRecycling(); + } + + @Override + protected ReactViewGroup prepareToRecycleView( + @NonNull ThemedReactContext reactContext, ReactViewGroup view) { + // BaseViewManager + super.prepareToRecycleView(reactContext, view); + + view.recycleView(); + + return view; + } + @ReactProp(name = "accessible") public void setAccessible(ReactViewGroup view, boolean accessible) { view.setFocusable(accessible);