diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java index 0403cc29a..4db2d180e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatNativeViewHierarchyManager.java @@ -11,6 +11,8 @@ package com.facebook.react.flat; import javax.annotation.Nullable; +import java.util.Collection; + import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; @@ -134,6 +136,30 @@ import com.facebook.react.uimanager.ViewManagerRegistry; } } + @Override + protected void dropView(View view) { + super.dropView(view); + + // As a result of removeClippedSubviews, some views have strong references but are not attached + // to a parent. consequently, when the parent gets removed, these Views don't get cleaned up, + // because they aren't children (they also aren't removed from mTagsToViews, thus causing a + // leak). To solve this, we ask for said detached views and explicitly drop them. + if (view instanceof FlatViewGroup) { + FlatViewGroup flatViewGroup = (FlatViewGroup) view; + if (flatViewGroup.getRemoveClippedSubviews()) { + Collection detachedViews = flatViewGroup.getDetachedViews(); + for (FlatViewGroup detachedChild : detachedViews) { + // we can do super here because removeClippedSubviews is currently not recursive. if/when + // we become recursive one day, this should call vanilla dropView to be recursive as well. + super.dropView(detachedChild); + // trigger onDetachedFromWindow - this is currently needed due to using attach/detach + // instead of add/remove. if we move to add/remove in the future, we can remove this. + flatViewGroup.removeDetachedView(detachedChild); + } + } + } + } + /* package */ void detachAllChildrenFromViews(int[] viewsToDetachAllChildrenFrom) { for (int viewTag : viewsToDetachAllChildrenFrom) { View view = resolveView(viewTag); diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java index ed23daade..e7811bbae 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/FlatViewGroup.java @@ -13,6 +13,7 @@ import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -407,6 +408,28 @@ import com.facebook.react.views.view.ReactClippingViewGroupHelper; invalidate(); } + /** + * Return a list of FlatViewGroups that are detached (due to being clipped) but that we have a + * strong reference to. This is used by the FlatNativeViewHierarchyManager to explicitly clean up + * those views when removing this parent. + * + * @return a Collection of FlatViewGroups to clean up + */ + Collection getDetachedViews() { + return mClippedSubviews.values(); + } + + /** + * Remove the detached view from the parent + * This is used during cleanup to trigger onDetachedFromWindow on any views that were in a + * temporary detached state due to them being clipped. This is called for cleanup of said views + * by FlatNativeViewHierarchyManager. + * @param flatViewGroup the detached FlatViewGroup to remove + */ + void removeDetachedView(FlatViewGroup flatViewGroup) { + removeDetachedView(flatViewGroup, false); + } + /* package */ void mountAttachDetachListeners(AttachDetachListener[] listeners) { if (mIsAttached) { // Ordering of the following 2 statements is very important. While logically it makes sense to