先从一段异常开始吧,这是在Activity中把布局上的一个TextView添加到另一个布局的时候抛出的一段异常:
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
代码我是这样写的:
RelativeLayout rl_main = (RelativeLayout) findViewById(R.id.rl_main);
View viewById = findViewById(R.id.tv_hello);
rl_main.addView(viewById);
1它是在什么时候报错了呢?
看这张异常图片
其实这张图片告诉了我们很多事情,比如:
ZygoteInit$MethodAndArgsCaller.run invoke了ActivityThread的main方法;
ActivityThread的H(一个叫H的handler)在handleMessage方法中调用了handleLaunchActivity
handleLaunchActivity是干嘛的呢?
handleLaunchActivity调用了ActivityThread的performLaunchActivity,然后Instrumentation.callActivityOnCreate。嘿嘿,这时候Activity.performCreate,接着我的MianActivity就走了onCreate方法。
这里只是回顾一下Activity的工作过程,毕竟不论你看不看异常就在这里,它会告诉你很多,平时开发你感受不到的东西。
真正出问题是在addView( )的时候,
在addViewInner()的时候会对添加进来的子view进行判断
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
原来如此,其实每个view都是有父view的引用的。是不是我把这个引用清除掉就行了,
/**
* The parent this view is attached to.
* {@hide}
*
* @see #getParent()
*/
protected ViewParent mParent;
/**
* Gets the parent of this view. Note that the parent is a
* ViewParent and not necessarily a View.
*
* @return Parent of this view.
*/
public final ViewParent getParent() {
return mParent;
}
首先这个mParent竟然是受保护的,
其次也没提供set方法。。。那如果想添加需要怎么做呢?
You must call removeView() on the child's parent
看看这里是怎么做到的:
public void removeView(View view) {
if (removeViewInternal(view)) {
requestLayout();
invalidate(true);
}
}
哦?如果移除成功就重新请求布局,并刷新页面,看来移除子view的代码在这里:
removeViewInternal(view):
private boolean removeViewInternal(View view) {
//返回view的index
final int index = indexOfChild(view);
//如果有这个view就移除
if (index >= 0) {
removeViewInternal(index, view);
return true;
}
return false;
}
这个方法主要也就是获取获取这个view的index原来移除view是需要角标啊, removeViewInternal(index, view) 发觉真相之前我们先认识下
// Used to animate add/remove changes in layout
// 在layout里 有生气的添加或者移除操作
// 这里的animate 是指动画吗?
private LayoutTransition mTransition;
// Used to manage the list of transient views, added by addTransientView()
// 一个用于管理临时view的集合
private List<Integer> mTransientIndices = null;
让我看看这里都发生了什么:
private void removeViewInternal(int index, View view) {
//一开始就移除了
if (mTransition != null) {
mTransition.removeChild(this, view);
}
//然后清除view的焦点
boolean clearChildFocus = false;
if (view == mFocused) {
view.unFocus(null);
clearChildFocus = true;
}
view.clearAccessibilityFocus();
//解除touch事件
cancelTouchTarget(view);
//解除hover事件
cancelHoverTarget(view);
//从window解除关联的view
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.hasTransientState()) {
childHasTransientStateChanged(view, false);
}
needGlobalAttributesUpdate(false);
//从viewgourp的内部数组中移除
removeFromArray(index);
if (clearChildFocus) {
clearChildFocus(view);
if (!rootViewRequestFocus()) {
notifyGlobalFocusCleared(this);
}
}
//从事件分发中移除
dispatchViewRemoved(view);
//如果这个view曾经的属性不是Gone则需要重新计算布局(把它占用在布局的位置清除掉)
if (view.getVisibility() != View.GONE) {
notifySubtreeAccessibilityStateChangedIfNeeded();
}
//从view的集合中把这个view移除掉
int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
final int oldIndex = mTransientIndices.get(i);
if (index < oldIndex) {
mTransientIndices.set(i, oldIndex - 1);
}
}
}
重点看怎么移除的: mTransition.removeChild(this, view),这里两个构造changesLayout默认传的是true
private void removeChild(ViewGroup parent, View child, boolean changesLayout) {
···
if (changesLayout &&
(mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
runChangeTransition(parent, child, DISAPPEARING);
}
if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
runDisappearingTransition(parent, child);
}
}
runChangeTransition(parent, child, DISAPPEARING) 设置布局改变动画DISAPPEARING
runDisappearingTransition(parent, child);执行动画并设置动画监听:
@Override
public void onAnimationEnd(Animator anim) {
currentDisappearingAnimations.remove(child);
child.setAlpha(preAnimAlpha);
if (hasListeners()) {
ArrayList<TransitionListener> listeners =
(ArrayList<TransitionListener>) mListeners.clone();
for (TransitionListener listener : listeners) {
listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING);
}
}
}
在ViewGroup中定义了这个listener并实现了回调:
private LayoutTransition.TransitionListener mLayoutTransitionListener =
new LayoutTransition.TransitionListener(){
···
@Override
public void endTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
···
if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
endViewTransition(view);
}
}
}
真相终于来了 在这里:
public void endViewTransition(View view) {
···
if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.mParent != null) {
view.mParent = null;
}
···
invalidate();
···
}
从window中移除,把mParent 置null;
看一下是怎么从window中移除的吧:
onDetachedFromWindow是个空实现方便开发者调用使用的;
这里主要看onDetachedFromWindowInternal()
/**
* This is a framework-internal mirror of onDetachedFromWindow() that's called
* after onDetachedFromWindow().
*
* If you override this you *MUST* call super.onDetachedFromWindowInternal()!
* The super method should be called at the end of the overridden method to ensure
* subclasses are destroyed first
*
* @hide
*/
@CallSuper
protected void onDetachedFromWindowInternal() {
mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
removeUnsetPressCallback();
removeLongPressCallback();
removePerformClickCallback();
removeSendViewScrolledAccessibilityEventCallback();
stopNestedScroll();
// Anything that started animating right before detach should already
// be in its final state when re-attached.
jumpDrawablesToCurrentState();
destroyDrawingCache();
cleanupDraw();
mCurrentAnimation = null;
}
//理论上view不能直接操作window而是通过viewrootimpl,在这个方法里貌似有了答案
private void cleanupDraw() {
resetDisplayList();
if (mAttachInfo != null) {
mAttachInfo.mViewRootImpl.cancelInvalidate(this);
}
}
进入ViewRootImpl:
public void cancelInvalidate(View view) {
mHandler.removeMessages(MSG_INVALIDATE, view);
// fixme: might leak the AttachInfo.InvalidateInfo objects instead of returning
// them to the pool
mHandler.removeMessages(MSG_INVALIDATE_RECT, view);
mInvalidateOnAnimationRunnable.removeView(view);
}
public void removeView(View view) {
synchronized (this) {
mViews.remove(view);
for (int i = mViewRects.size(); i-- > 0; ) {
AttachInfo.InvalidateInfo info = mViewRects.get(i);
if (info.target == view) {
mViewRects.remove(i);
info.recycle();
}
}
if (mPosted && mViews.isEmpty() && mViewRects.isEmpty()) {
mChoreographer.removeCallbacks(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = false;
}
}
}
移除view并不是把这个view删除掉而是把它从它的parentView中移除,然而移除的过程中有可能需要执行动画,更多的是将它从viewgroup的子view集合中移除并告诉window这个view没有了并进行requestLayout();所以不光是修改UI的内容要在主线程中,只要是涉及到UI重绘 的操作都需要放到viewrootimpl所在的线程中(默认是在主线程);
网友评论