Skip to content

Commit

Permalink
Prototype View recycling for View
Browse files Browse the repository at this point in the history
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
  • Loading branch information
JoshuaGross authored and facebook-github-bot committed May 26, 2022
1 parent 0ef73f2 commit 7b778fb
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ public void run() {
mRootViewManager = null;
mMountItemExecutor = null;
mOnViewAttachItems.clear();

if (ReactFeatureFlags.enableViewRecycling) {
mViewManagerRegistry.onSurfaceStopped(mSurfaceId);
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
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
Expand All @@ -33,6 +36,30 @@
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.
Expand Down Expand Up @@ -137,7 +164,13 @@ public C createShadowNodeInstance() {
@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) {
Expand All @@ -157,7 +190,27 @@ public C createShadowNodeInstance() {
* 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
Expand Down Expand Up @@ -370,4 +423,24 @@ public long measure(
* 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<>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -42,6 +46,40 @@ public ViewManagerRegistry(Map<String, ViewManager> viewManagerMap) {
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
Expand Down Expand Up @@ -91,4 +129,22 @@ ViewManager getViewManagerIfExists(String className) {
}
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public ReactViewBackgroundManager(View view) {
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());
Expand Down
Loading

0 comments on commit 7b778fb

Please sign in to comment.