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
This commit is contained in:
Joshua Gross
2022-05-26 03:40:21 -07:00
committed by Facebook GitHub Bot
parent 0ef73f2a82
commit 7b778fbebb
9 changed files with 293 additions and 11 deletions
@@ -119,4 +119,9 @@ public class ReactFeatureFlags {
* </ul>
*/
public static int turboModuleBindingMode = 0;
/**
* Feature Flag to enable View Recycling. When enabled, individual ViewManagers must still opt-in.
*/
public static boolean enableViewRecycling = false;
}
@@ -304,6 +304,10 @@ public class SurfaceMountingManager {
mRootViewManager = null;
mMountItemExecutor = null;
mOnViewAttachItems.clear();
if (ReactFeatureFlags.enableViewRecycling) {
mViewManagerRegistry.onSurfaceStopped(mSurfaceId);
}
}
};
@@ -64,6 +64,48 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
private static final String STATE_EXPANDED = "expanded";
private static final String STATE_MIXED = "mixed";
@Override
protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T view) {
// Reset tags
view.setTag(R.id.pointer_enter, null);
view.setTag(R.id.pointer_leave, null);
view.setTag(R.id.pointer_move, null);
view.setTag(R.id.react_test_id, null);
view.setTag(R.id.view_tag_native_id, null);
view.setTag(R.id.labelled_by, null);
view.setTag(R.id.accessibility_label, null);
view.setTag(R.id.accessibility_hint, null);
view.setTag(R.id.accessibility_role, null);
view.setTag(R.id.accessibility_state, null);
view.setTag(R.id.accessibility_actions, null);
view.setTag(R.id.accessibility_value, null);
// This indirectly calls (and resets):
// setTranslationX
// setTranslationY
// setRotation
// setRotationX
// setRotationY
// setScaleX
// setScaleY
// setCameraDistance
setTransform(view, null);
// RenderNode params not covered by setTransform above
view.setPivotX(0);
view.setPivotY(0);
view.setTop(0);
view.setBottom(0);
view.setLeft(0);
view.setRight(0);
view.setElevation(0);
view.setAnimationMatrix(null);
view.setOutlineAmbientShadowColor(Color.BLACK);
view.setOutlineSpotShadowColor(Color.BLACK);
return view;
}
@Override
@ReactProp(
name = ViewProps.BACKGROUND_COLOR,
@@ -16,13 +16,16 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.annotations.ReactPropertyHolder;
import com.facebook.yoga.YogaMeasureMode;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
/**
* Class responsible for knowing how to create and update catalyst Views of a given type. It is also
@@ -33,6 +36,30 @@ import java.util.Map;
public abstract class ViewManager<T extends View, C extends ReactShadowNode>
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<Integer, Stack<T>> mRecyclableViews = null;
/** Call in constructor of concrete ViewManager class to enable. */
protected void enableViewRecycling() {
if (ReactFeatureFlags.enableViewRecycling) {
mRecyclableViews = new HashMap<>();
}
}
private @Nullable Stack<T> 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<T extends View, C extends ReactShadowNode>
@NonNull ThemedReactContext reactContext,
@Nullable ReactStylesDiffMap initialProps,
@Nullable StateWrapper stateWrapper) {
T view = createViewInstance(reactContext);
T view = null;
@Nullable Stack<T> 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<T extends View, C extends ReactShadowNode>
* 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<T> 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<T extends View, C extends ReactShadowNode>
* 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<>();
}
}
}
@@ -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<String, ViewManager> 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<String, ViewManager> 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<String, ViewManager> entry : mViewManagers.entrySet()) {
entry.getValue().onSurfaceStopped(surfaceId);
}
}
};
if (UiThreadUtil.isOnUiThread()) {
runnable.run();
} else {
UiThreadUtil.runOnUiThread(runnable);
}
}
}
@@ -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"),
],
)
@@ -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());
@@ -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);
}
@@ -52,6 +52,25 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
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);