mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-04 22:56:32 +08:00
Summary: This fixes a crash for the case when we try to drop a view that has already been dropped. **The Problem** We got reports of a crash (t12912526) that occurs when the resolveViewManager method can't resolve a ViewManager for a View being dropped. Investigating this, one thing in common between all the stack traces for this is that dropView is called from line 210 of FlatNativeViewHierarchyManager. This part of the code is specifically the part we added to remove strong references to any clipped children (from views that have subview clipping enabled). So this is a problem specifically with Nodes and clipSubviews, which brings up some questions: **when can this happen?** The only situation this can possibly happen is when we drop a child (which is clipped) followed by dropping its parent in the same cycle. Consider a tree where each view only has one child, such as: A - B - C - D. This crash would happen if D is clipped, and we removed it, followed by removing any of its parents in the same frame. **if the removes happen in different frames, does this bug occur?** No - the reason is that before we execute the DropView operations, we run through StateBuilder, which traverses the shadow tree and marks updates, thus removing the view going away (such that the delete in the next frame doesn't try to re-delete it). So why doesn't this happen when we're dropping in the same frame? The reason is that manageChildren (where this all starts) asks to remove some views. We handle this by removing said Nodes and their children from the shadow tree. Consequently, when StateBuilder iterates over the shadow tree, it can't do the right thing because said nodes no longer exist. As a more concrete example, consider A - B - C - D again, and consider that both D and B are removed. StateBuilder only sees A, and realizes that it now has 0 children (whereas before it has 1), so it removes B from its children. However, this process isn't recursive, so C never gets cleaned up. **why doesn't this happen with Nodes without clipping containers?** The answer to this is that NativeViewHierarchyManager's dropView method checks the existance of each child before deeply dropping that child and its subtree. So in this case, we drop D and all its children, and when we come to drop B, we try to drop C (which exists) and then its children (D, which doesn't exist because we already dropped it, so we ignore it). **why doesn't this happen with non-Nodes?** The reason is that non-Nodes handles removes differently - every remove is enqueued in a call to NativeViewHierarchy's manageChildren, which explicitly asks the parent to remove said child. Consequently, we never try to remove a child that is already removed. **Fix** The initial fix was to check whether or not the view exists, but this updated patch just does the right thing at drop time - i.e. whenever a view is dropped, we notify the parent of this fact so that it can clear the reference from clipped views. **One last Note** There are two reasons for switching `super.dropView` to `dropView` - first, the comment is only partially correct - calling `super.dropView` will avoid looking at clipped children (as an aside, that could cause a leak in the case of nested clipping subviews), but will look at clipped grandchildren, because of the super class's iteration across the set of children. Reviewed By: astreet Differential Revision: D3815485
254 lines
9.7 KiB
Java
254 lines
9.7 KiB
Java
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
package com.facebook.react.flat;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import android.util.SparseArray;
|
|
import android.util.SparseIntArray;
|
|
import android.view.View;
|
|
import android.view.View.MeasureSpec;
|
|
import android.view.ViewGroup;
|
|
|
|
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
|
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
|
|
import com.facebook.react.uimanager.ThemedReactContext;
|
|
import com.facebook.react.uimanager.ViewGroupManager;
|
|
import com.facebook.react.uimanager.ViewManagerRegistry;
|
|
|
|
/**
|
|
* FlatNativeViewHierarchyManager is the only class that performs View manipulations. All of this
|
|
* class methods can only be called from UI thread by {@link FlatUIViewOperationQueue}.
|
|
*/
|
|
/* package */ final class FlatNativeViewHierarchyManager extends NativeViewHierarchyManager
|
|
implements ViewResolver {
|
|
|
|
/* package */ FlatNativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
|
|
super(viewManagers, new FlatRootViewManager());
|
|
}
|
|
|
|
@Override
|
|
public View getView(int reactTag) {
|
|
return super.resolveView(reactTag);
|
|
}
|
|
|
|
@Override
|
|
public void addRootView(
|
|
int tag,
|
|
SizeMonitoringFrameLayout view,
|
|
ThemedReactContext themedContext) {
|
|
FlatViewGroup root = new FlatViewGroup(themedContext);
|
|
view.addView(root);
|
|
|
|
// When unmounting, ReactInstanceManager.detachViewFromInstance() will check id of the
|
|
// top-level View (SizeMonitoringFrameLayout) and pass it back to JS. We want that View's id to
|
|
// be set, otherwise NativeViewHierarchyManager will not be able to cleanup properly.
|
|
view.setId(tag);
|
|
|
|
addRootViewGroup(tag, root, themedContext);
|
|
}
|
|
|
|
/**
|
|
* Updates DrawCommands and AttachDetachListeners of a FlatViewGroup specified by a reactTag.
|
|
*
|
|
* @param reactTag reactTag to lookup FlatViewGroup by
|
|
* @param drawCommands if non-null, new draw commands to execute during the drawing.
|
|
* @param listeners if non-null, new attach-detach listeners.
|
|
*/
|
|
/* package */ void updateMountState(
|
|
int reactTag,
|
|
@Nullable DrawCommand[] drawCommands,
|
|
@Nullable AttachDetachListener[] listeners,
|
|
@Nullable NodeRegion[] nodeRegions) {
|
|
FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
|
|
if (drawCommands != null) {
|
|
view.mountDrawCommands(drawCommands);
|
|
}
|
|
if (listeners != null) {
|
|
view.mountAttachDetachListeners(listeners);
|
|
}
|
|
if (nodeRegions != null) {
|
|
view.mountNodeRegions(nodeRegions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates DrawCommands and AttachDetachListeners of a clipping FlatViewGroup specified by a
|
|
* reactTag.
|
|
*
|
|
* @param reactTag The react tag to lookup FlatViewGroup by.
|
|
* @param drawCommands If non-null, new draw commands to execute during the drawing.
|
|
* @param drawViewIndexMap Mapping of react tags to the index of the corresponding DrawView
|
|
* command in the draw command array.
|
|
* @param commandMaxBot At each index i, the maximum bottom value (or right value in the case of
|
|
* horizontal clipping) value of all draw commands at or below i.
|
|
* @param commandMinTop At each index i, the minimum top value (or left value in the case of
|
|
* horizontal clipping) value of all draw commands at or below i.
|
|
* @param listeners If non-null, new attach-detach listeners.
|
|
* @param nodeRegions Node regions to mount.
|
|
* @param regionMaxBot At each index i, the maximum bottom value (or right value in the case of
|
|
* horizontal clipping) value of all node regions at or below i.
|
|
* @param regionMinTop At each index i, the minimum top value (or left value in the case of
|
|
* horizontal clipping) value of all draw commands at or below i.
|
|
* @param willMountViews Whether we are going to also send a mountViews command in this state
|
|
* cycle.
|
|
*/
|
|
/* package */ void updateClippingMountState(
|
|
int reactTag,
|
|
@Nullable DrawCommand[] drawCommands,
|
|
SparseIntArray drawViewIndexMap,
|
|
float[] commandMaxBot,
|
|
float[] commandMinTop,
|
|
@Nullable AttachDetachListener[] listeners,
|
|
@Nullable NodeRegion[] nodeRegions,
|
|
float[] regionMaxBot,
|
|
float[] regionMinTop,
|
|
boolean willMountViews) {
|
|
FlatViewGroup view = (FlatViewGroup) resolveView(reactTag);
|
|
if (drawCommands != null) {
|
|
view.mountClippingDrawCommands(
|
|
drawCommands,
|
|
drawViewIndexMap,
|
|
commandMaxBot,
|
|
commandMinTop,
|
|
willMountViews);
|
|
}
|
|
if (listeners != null) {
|
|
view.mountAttachDetachListeners(listeners);
|
|
}
|
|
if (nodeRegions != null) {
|
|
view.mountClippingNodeRegions(nodeRegions, regionMaxBot, regionMinTop);
|
|
}
|
|
}
|
|
|
|
/* package */ void updateViewGroup(int reactTag, int[] viewsToAdd, int[] viewsToDetach) {
|
|
View view = resolveView(reactTag);
|
|
if (view instanceof FlatViewGroup) {
|
|
((FlatViewGroup) view).mountViews(this, viewsToAdd, viewsToDetach);
|
|
return;
|
|
}
|
|
|
|
ViewGroup viewGroup = (ViewGroup) view;
|
|
ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(reactTag);
|
|
List<View> listOfViews = new ArrayList<>(viewsToAdd.length);
|
|
|
|
// batch the set of additions - some view managers can take advantage of the batching to
|
|
// decrease operations, etc.
|
|
for (int viewIdToAdd : viewsToAdd) {
|
|
int tag = Math.abs(viewIdToAdd);
|
|
listOfViews.add(resolveView(tag));
|
|
}
|
|
viewManager.addViews(viewGroup, listOfViews);
|
|
}
|
|
|
|
/**
|
|
* Updates View bounds, possibly re-measuring and re-layouting it if the size changed.
|
|
*
|
|
* @param reactTag reactTag to lookup a View by
|
|
* @param left left coordinate relative to parent
|
|
* @param top top coordinate relative to parent
|
|
* @param right right coordinate relative to parent
|
|
* @param bottom bottom coordinate relative to parent
|
|
*/
|
|
/* package */ void updateViewBounds(int reactTag, int left, int top, int right, int bottom) {
|
|
View view = resolveView(reactTag);
|
|
int width = right - left;
|
|
int height = bottom - top;
|
|
if (view.getWidth() != width || view.getHeight() != height) {
|
|
// size changed, we need to measure and layout the View
|
|
view.measure(
|
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
|
view.layout(left, top, right, bottom);
|
|
} else {
|
|
// same size, only location changed, there is a faster route.
|
|
view.offsetLeftAndRight(left - view.getLeft());
|
|
view.offsetTopAndBottom(top - view.getTop());
|
|
}
|
|
}
|
|
|
|
/* package */ void setPadding(
|
|
int reactTag,
|
|
int paddingLeft,
|
|
int paddingTop,
|
|
int paddingRight,
|
|
int paddingBottom) {
|
|
resolveView(reactTag).setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
|
|
}
|
|
|
|
/* package */ void dropViews(SparseIntArray viewsToDrop) {
|
|
for (int i = 0, count = viewsToDrop.size(); i < count; i++) {
|
|
int viewToDrop = viewsToDrop.keyAt(i);
|
|
View view = null;
|
|
if (viewToDrop > 0) {
|
|
view = resolveView(viewToDrop);
|
|
dropView(view);
|
|
} else {
|
|
// Root views are noted with a negative tag from StateBuilder.
|
|
removeRootView(-viewToDrop);
|
|
}
|
|
|
|
int parentTag = viewsToDrop.valueAt(i);
|
|
// this only happens for clipped, non-root views - clipped because there is no parent, and
|
|
// not a root view (because we explicitly pass -1 for root views).
|
|
if (parentTag > 0 && view != null && view.getParent() == null) {
|
|
// this can only happen if the parent exists (if the parent were removed first, it'd also
|
|
// remove the child, so trying to explicitly remove the child afterwards would crash at
|
|
// the resolveView call above) - we also explicitly check for a null parent, implying that
|
|
// we are either clipped (or that we already removed the child from its parent, in which
|
|
// case this will essentially be a no-op).
|
|
View parent = resolveView(parentTag);
|
|
if (parent instanceof FlatViewGroup) {
|
|
((FlatViewGroup) parent).onViewDropped(view);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@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()) {
|
|
SparseArray<View> detachedViews = flatViewGroup.getDetachedViews();
|
|
for (int i = 0, size = detachedViews.size(); i < size; i++) {
|
|
View detachedChild = detachedViews.valueAt(i);
|
|
dropView(detachedChild);
|
|
// trigger onDetachedFromWindow and clean up this detached/clipped view
|
|
flatViewGroup.removeDetachedView(detachedChild);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* package */ void detachAllChildrenFromViews(int[] viewsToDetachAllChildrenFrom) {
|
|
for (int viewTag : viewsToDetachAllChildrenFrom) {
|
|
View view = resolveView(viewTag);
|
|
if (view instanceof FlatViewGroup) {
|
|
((FlatViewGroup) view).detachAllViewsFromParent();
|
|
continue;
|
|
}
|
|
|
|
ViewGroup viewGroup = (ViewGroup) view;
|
|
ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(viewTag);
|
|
viewManager.removeAllViews(viewGroup);
|
|
}
|
|
}
|
|
}
|