如果文章有问题,请及时指出
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进行添加操作。**
demo源码:github.com/fengyun703/…