使用animateLayoutChanges属性后,添加和移除view崩溃

1,515 阅读8分钟

如果文章有问题,请及时指出

android 28 源码

一 背景

在工作中,碰到一个奇怪崩溃。
场景是这样的:在ActivityA打开自己,ActivityA为singleTask。在onNewIntent(),会把一个ViewA从ViewGroupB中移除,发送网络请,请求返回后,把ViewA添加到ViewGroupB中(这逻辑感觉有问题,但确实是这样的)。在ViewGroupB.addView(ViewA)时,发生了崩溃。
崩溃日志:
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

二 原因分析

根据日志,崩溃原因很明显:addView时,子view已经有父容器了。
ViewGroup.java

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {

    if (mTransition != null) {
        // Don't prevent other add transitions from completing, but cancel remove
        // transitions to let them complete the process before we add to the container
        mTransition.cancel(LayoutTransition.DISAPPEARING);
    }

    if (child.getParent() != null) {
        throw new IllegalStateException("The specified child already has a parent. " +
                "You must call removeView() on the child's parent first.");
    }
    .......
}
不管代码逻辑问题,只是好奇技术原因是啥。先移除后添加为什么会崩溃,这不合理。
removeView时,没有把mParent置空。那什么时候把mParent置空呢?只能从系统源码中查找了。开始跟踪ViewGroup.removeView()源码,与mTransitioningViews 有关联。
ViewGroup.java 

private void removeViewInternal(int index, View view) {
    // 这里会把View加到动画view集合里,开始过渡动画
    if (mTransition != null) {
        mTransition.removeChild(this, view);
    }
    ......
    removeFromArray(index);
    ......
}

private void removeFromArray(int index) {
    final View[] children = mChildren;
    //  在过渡动画集合中,就不吧mParent置null
    if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
        children[index].mParent = null;
    }
    final int count = mChildrenCount;
    // 这里把children[]中对应view置null
    if (index == count - 1) {
        children[--mChildrenCount] = null;
    } else if (index >= 0 && index < count) {
        System.arraycopy(children, index + 1, children, index, count - index - 1);
        children[--mChildrenCount] = null;
    } else {
        throw new IndexOutOfBoundsException();
    }
    if (mLastTouchDownIndex == index) {
        mLastTouchDownTime = 0;
        mLastTouchDownIndex = -1;
    } else if (mLastTouchDownIndex > index) {
        mLastTouchDownIndex--;
    }
}
接下来看mTransitioningViews 什么时候add的?是在LayoutTransition的回调中调用的startViewTransition(view),向mTransitioningViews 添加view。
private LayoutTransition.TransitionListener mLayoutTransitionListener =
        new LayoutTransition.TransitionListener() {
    @Override
    public void startTransition(LayoutTransition transition, ViewGroup container,
            View view, int transitionType) {
        // We only care about disappearing items, since we need special logic to keep
        // those items visible after they've been 'removed'
        if (transitionType == LayoutTransition.DISAPPEARING) {
            startViewTransition(view);
        }
    }

    @Override
    public void endTransition(LayoutTransition transition, ViewGroup container,
            View view, int transitionType) {
        if (mLayoutCalledWhileSuppressed && !transition.isChangingLayout()) {
            requestLayout();
            mLayoutCalledWhileSuppressed = false;
        }
        if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
            endViewTransition(view);
        }
    }
};

public void startViewTransition(View view) {
    if (view.mParent == this) {
        if (mTransitioningViews == null) {
            mTransitioningViews = new ArrayList<View>();
        }
        mTransitioningViews.add(view);
    }
}

public void endViewTransition(View view) {
    if (mTransitioningViews != null) {
        mTransitioningViews.remove(view);
        // 这里出现 mDisappearingChildren
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        if (disappearingChildren != null && disappearingChildren.contains(view)) {
            disappearingChildren.remove(view);
            // 这里出现 mVisibilityChangingChildren
            if (mVisibilityChangingChildren != null &&
                    mVisibilityChangingChildren.contains(view)) {
                mVisibilityChangingChildren.remove(view);
            } else {
                if (view.mAttachInfo != null) {
                    view.dispatchDetachedFromWindow();
                }
                // 这里才把mParent置null
                if (view.mParent != null) {
                    view.mParent = null;
                }
            }
            invalidate();
        }
    }
}
接下来看mLayoutTransitionListener怎么使用的?看到是在解析xml属性时初始化mTransition,并把mLayoutTransitionListener添加到其中。
ViewGroup.java 

private void initFromAttributes(
        Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
   final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup, defStyleAttr,
        defStyleRes);
    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
    int attr = a.getIndex(i);
    switch (attr) {
        case R.styleable.ViewGroup_animateLayoutChanges:
            boolean animateLayoutChanges = a.getBoolean(attr, false);
            if (animateLayoutChanges) {
                setLayoutTransition(new LayoutTransition());
            }
            break;
       .......
       }
}

public void setLayoutTransition(LayoutTransition transition) {
    if (mTransition != null) {
        LayoutTransition previousTransition = mTransition;
        previousTransition.cancel();
        previousTransition.removeTransitionListener(mLayoutTransitionListener);
    }
    mTransition = transition;
    if (mTransition != null) {
        mTransition.addTransitionListener(mLayoutTransitionListener);
    }
}
**到这里其实我们可给出临时解决方案:在我们app代码中,没有主动调用过setLayoutTransition(),那只要把xml 中android:animateLayoutChanges="true"删除,动画功能关闭了,就不会有这问题。验证没问题,决定先这么解决,保证发版正常。**
但最终原因还是没有查出来。接下来我们一起探案下。
其实接下来该研究LayoutTransition代码,但这个类过于复杂,很多动画逻辑,如果完全弄懂,要花太久时间。我打算偷懒了,使用反射等方式。**既然知道这个问题和mTransitioningViews、mVisibilityChangingChildren、mDisappearingChildren三个成员有关。和TransitionListener有关**
我写了个demo,xml中使用xml 中android:animateLayoutChanges="true",反射获得这几个成员变量,打印其中值来。并添加一个TransitionListener。
这里碰到个小插曲:targetSdkVersion 28,反射获得成员变量会失败,因为私有api禁用原因。只能把targetSdkVersion 改小。为了验证问题,只能暂时把公司app和demo的targetSdkVersion 改成27了。
demo代码主要逻辑如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mParent = findViewById(R.id.parent);
    mChild = findViewById(R.id.child);

    mLayoutTransition = new MyLayoutTransition();
    // 先添加我们自己的监听,保证我们自己的先于系统的执行。
    mLayoutTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
        @Override
        public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
            try {
                Log.i(TAG, "startTransition: mTransitioningViewsField = " + mTransitioningViewsField.get(mParent));
                Log.i(TAG, "startTransition: mDisappearingChildrenField = " + mDisappearingChildrenField.get(mParent));
                Log.i(TAG, "startTransition: mVisibilityChangingChildrenField = " + mVisibilityChangingChildrenField.get(mParent));
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.i(TAG, "startTransition: parent = " + mParent + ", child = " + mChild + ". child.getParent =" + mChild.getParent() + ", transitionType =" + transitionType);
        }

        @Override
        public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
            try {
                Log.i(TAG, "endTransition: mTransitioningViewsField = " + mTransitioningViewsField.get(mParent));
                Log.i(TAG, "endTransition: mDisappearingChildrenField = " + mDisappearingChildrenField.get(mParent));
                Log.i(TAG, "endTransition: mVisibilityChangingChildrenField = " + mVisibilityChangingChildrenField.get(mParent));
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.i(TAG, "endTransition: parent = " + mParent + ", child = " + mChild + ". child.getParent =" + mChild.getParent() + ", transitionType =" + transitionType );
        }
    });
    mParent.setLayoutTransition(mLayoutTransition);
    try {
        mTransitioningViewsField = ViewGroup.class.getDeclaredField("mTransitioningViews");
        mTransitioningViewsField.setAccessible(true);
        mVisibilityChangingChildrenField = ViewGroup.class.getDeclaredField("mVisibilityChangingChildren");
        mVisibilityChangingChildrenField.setAccessible(true);
        mDisappearingChildrenField = ViewGroup.class.getDeclaredField("mDisappearingChildren");
        mDisappearingChildrenField.setAccessible(true);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static class MyLayoutTransition extends LayoutTransition {

    @Override
    public void addChild(ViewGroup parent, View child) {
        Log.i(TAG, "addChild1: parent = " + parent + ", child = " + child + ". child.getParent =" + child.getParent());
        super.addChild(parent, child);
        Log.i(TAG, "addChild2: parent = " + parent + ", child = " + child + ". child.getParent =" + child.getParent());

    }

    @Override
    public void removeChild(ViewGroup parent, View child) {
        Log.i(TAG, "removeChild1: parent = " + parent + ", child = " + child + ". child.getParent =" + child.getParent());
        super.removeChild(parent, child);
        Log.i(TAG, "removeChild2: parent = " + parent + ", child = " + child + ". child.getParent =" + child.getParent());
    }
}

public void remove1(View view) {
    Log.i(TAG, "remove 1: view.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 1: parent.getChildAt(0) = " + mParent.getChildAt(0));
    mParent.removeView(mChild);
    Log.i(TAG, "remove 2: view.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 2: parent.getChildAt(0) = " + mParent.getChildAt(0));
    mParent.addView(mChild, 0);
}
日志:
发现在我们自己监听的endTransition时,mParent还不为null,在addview时,mParent为null了。ViewGroup.endViewTransition()正常执行,会把mParent置空。但公司app为啥不行。
remove 1: view.getParent() = android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}
 remove 1: parent.getChildAt(0) = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}

 removeChild1: parent = android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}. child.getParent =android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}

startTransition: mTransitioningViewsField = null
startTransition: mDisappearingChildrenField = null
startTransition: mVisibilityChangingChildrenField = null
 startTransition: parent = android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}. child.getParent =android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}, transitionType =3

removeChild2: parent = android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}. child.getParent =android.widget.LinearLayout{578687c V.E...... ........ 0,0-1080,1981 #7f030002 app:id/parent}

remove 2: view.getParent() = android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}
remove 2: parent.getChildAt(0) = android.widget.Button{791895a VFED..C.. ...P.... 419,551-661,683}

endTransition: mTransitioningViewsField = [android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}]
endTransition: mDisappearingChildrenField = [android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}]
endTransition: mVisibilityChangingChildrenField = null
endTransition: parent = android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}. child.getParent =android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}, transitionType =3
// 这里 mParent= null
addChild1: parent = android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ......ID 402,138-677,413 #7f030000 app:id/child}. child.getParent =null

startTransition: mTransitioningViewsField = []
startTransition: mDisappearingChildrenField = []
startTransition: mVisibilityChangingChildrenField = null
startTransition: parent = android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ......ID 402,138-677,413 #7f030000 app:id/child}. child.getParent =null, transitionType =2

addChild2: parent = android.widget.LinearLayout{578687c V.E...... ......ID 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ......ID 402,138-677,413 #7f030000 app:id/child}. child.getParent =null

endTransition: mTransitioningViewsField = []
endTransition: mDisappearingChildrenField = []
endTransition: mVisibilityChangingChildrenField = null
endTransition: parent = android.widget.LinearLayout{578687c V.E...... .......D 0,0-1080,1981 #7f030002 app:id/parent}, child = android.widget.TextView{27505 V.ED..... ........ 402,138-677,413 #7f030000 app:id/child}. child.getParent =android.widget.LinearLayout{578687c V.E...... .......D 0,0-1080,1981 #7f030002 app:id/parent}, transitionType =2
我把这些逻辑加到公司app中。
对比日志发现是mDisappearingChildren有问题。在removeView之后,addView之前的某个环节把mDisappearingChildren中view清空,导致ViewGroup.endViewTransition()执行没有把child.mParent置null。
我接下里把**mDisappearingChildren使用自己重写的ArrayList替代**。把元素操作方法都打印出来。终于发现问题了,是中间调用**mDisappearingChildren.clear()**方法。结合断点,终于找到原因了。是在Activity的onResume中用DecorView进行View移除和添加操作,把用户布局又包了一层(具体原因不清楚)。这时会调用我们布局View的dispatchDetachedFromWindow,调用了mDisappearingChildren.clear(),这样我们add时会报错。
我按照这场景编写了demo,终于找到必须路径了。
布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/content"
        android:orientation="vertical"
        android:background="#22ff0000"
        android:animateLayoutChanges="true"
        android:padding="20dp"
        android:gravity="center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/child"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="50dp"
            android:background="#33ff00ff"
            android:gravity="center"
            android:text="Hello World!" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:onClick="remove1"
            android:text="remove1" />

    </LinearLayout>

</LinearLayout>
主要逻辑代码:
/**
 * 崩溃模拟1
 * @param view
 */
public void remove1(View view) {
    mContent.removeViewAt(0);
    Log.i(TAG, "remove 1: mChild.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 1: mContent.getChildAt(0) = " + mContent.getChildAt(0));

    View view1 = mParent.getChildAt(0);
    mParent.removeViewAt(0);
    FrameLayout fl = new FrameLayout(this);
    fl.addView(view1);
    mParent.addView(fl, 0);

    Log.i(TAG, "remove 2: mChild.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 2: mContent.getChildAt(0) = " + mContent.getChildAt(0));

    mContent.addView(mChild, 0);
    Log.i(TAG, "remove 3: mChild.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 3: mContent.getChildAt(0) = " + mContent.getChildAt(0));

}

/**
 * 崩溃模拟2
 * @param view
 */
public void remove2(View view) {
    View child = mContent.getChildAt(0);
    mContent.removeViewAt(0);
    Log.i(TAG, "remove 4: mChild.getParent() = " + mChild.getParent());
    Log.i(TAG, "remove 4: mContent.getChildAt(0) = " + mContent.getChildAt(0));
    mParent.addView(child);
}

根据场景1,我发现新的崩溃场景,把子view移除,添加到父父view上,也会崩溃。同级添加是没问题的,但跨级添加会出问题。因为child.mParent只有动画结束才会置null,添加到原来parent会触发cancel操作,如果添加到其他父view是没有这个操作的。

**总结:终于找到具体原因了。在removeView和addView之间,把mDisappearingChildren清空了,到addview时子view的mParent没有置空,所以崩溃了。这应该是逻辑有些问题,导致系统bug。在使用animateLayoutChanges属性后,添加和移除子view要十分小心,移除view后,不要马上对移除view进行添加操作。**