The specified child already has a parent. You must call removeView() on the child's parent first.

先从一段异常开始吧,这是在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所在的线程中(默认是在主线程);


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值