mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
0ef73f2a82
commit
7b778fbebb
@@ -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;
|
||||
}
|
||||
|
||||
+4
@@ -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"),
|
||||
],
|
||||
)
|
||||
|
||||
+6
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user