美文网首页
源码分析同一个View为什么不能被ViewGroup连续addV

源码分析同一个View为什么不能被ViewGroup连续addV

作者: CyanStone | 来源:发表于2018-06-23 16:04 被阅读0次

    引言

    在日常开发过程中我们肯定遇到过,同一个View如果连续被add两次,会报出下边的错误:

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

    错误信息告诉我们此时这个View已经有了parent,并提示我们应该这个view的父容器,在addView之前,应该先调用removeView()方法。

    源码分析

    1. ViewGroup.addView()
        public void addView(View child) {
            addView(child, -1);
        }
    
        public void addView(View child, int index) {
            LayoutParams params = child.getLayoutParams();
            if (params == null) {
                params = generateDefaultLayoutParams();
                if (params == null) {
                    throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
                }
            }
            addView(child, index, params);
        }
    
        public void addView(View child, int width, int height) {
            final LayoutParams params = generateDefaultLayoutParams();
            params.width = width;
            params.height = height;
            addView(child, -1, params);
        }
    
        public void addView(View child, int index, LayoutParams params) {
            if (DBG) {
                System.out.println(this + " addView");
            }
    
            // addViewInner() will call child.requestLayout() when setting the new LayoutParams
            // therefore, we call requestLayout() on ourselves before, so that the child's request
            // will be blocked at our level
            requestLayout();
            invalidate(true);
            addViewInner(child, index, params, false);
        }
    

    从上述源码可以看出,调用addView(View child)方法,其实最后调用的是addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout):

     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);
            }
            //检查child的mParent是否为空
            if (child.getParent() != null) {
                throw new IllegalStateException("The specified child already has a parent. " +
                        "You must call removeView() on the child's parent first.");
            }
            if (mTransition != null) {
                mTransition.addChild(this, child);
            }
    
            if (!checkLayoutParams(params)) {
                params = generateLayoutParams(params);
            }
    
            if (preventRequestLayout) {
                child.mLayoutParams = params;
            } else {
                child.setLayoutParams(params);
            }
    
            if (index < 0) {
                index = mChildrenCount;
            }
    
            addInArray(child, index);
    
            // tell our children
            //在这里给view的mParent赋值的
            if (preventRequestLayout) {
                child.assignParent(this);
            } else {
                child.mParent = this;
            }
    
            if (child.hasFocus()) {
                requestChildFocus(child, child.findFocus());
            }
    
            AttachInfo ai = mAttachInfo;
            if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
                boolean lastKeepOn = ai.mKeepScreenOn;
                ai.mKeepScreenOn = false;
                child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
                if (ai.mKeepScreenOn) {
                    needGlobalAttributesUpdate(true);
                }
                ai.mKeepScreenOn = lastKeepOn;
            }
    
            if (child.isLayoutDirectionInherited()) {
                child.resetRtlProperties();
            }
    
            onViewAdded(child);
    
            if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
                mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
           }
    
            if (child.hasTransientState()) {
                childHasTransientStateChanged(child, true);
            }
    
            if (child.isImportantForAccessibility() && child.getVisibility() != View.GONE) {
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
          
        }
    

    这个方法的逻辑比较多,其他的我们先不作分析,从这里可以看到,如果child.getParent() != null,就会抛出这个异常。

    2. View的getParent()
        /**
         * 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;
        }
    
    

    View的getParent()方法返回的是mParent对象,并不是一个View对象,而是ViewParent实现类的对象。ViewParent是一个接口,定义了一组子View与Parent交互的API。ViewGroup是ViewParent接口实现类。下面我们追踪下mParent对象是在怎么被赋值的。

        /*
         * Caller is responsible for calling requestLayout if necessary.
         * (This allows addViewInLayout to not request a new layout.)
         */
        void assignParent(ViewParent parent) {
            if (mParent == null) {
                mParent = parent;
            } else if (parent == null) {
                mParent = null;
            } else {
                throw new RuntimeException("view " + this + " being added, but"
                        + " it already has a parent");
            }
        }
    

    追踪可以发现,View的mParent是在assignParent()方法中被赋值的,在该方法中,如果mParent != null && parent != null的时候,会抛出"view XXX being added, but it already has a parent"异常,这个异常其实是跟ViewGroup抛出的那个异常是对应的,算是一种Double Check吧。

    assignParent()方法是在ViewGroup的rivate void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout)方法有被被调用,从上边给出的ViewGroup->addViewInner()的源码可知,如果preventRequestLayout为true的情况下,ViewGroup就会调用 child.assignParent(this),给child的mParent对象进行赋值。否则,就会直接把自己复制给child的mParent。

    3. ViewGroup->removeView
        public void removeView(View view) {
            removeViewInternal(view);
            requestLayout();
            invalidate(true);
        }
    

    requestLayout()和invalidate(true)分别是请求对其重新测量和重绘,下边重点来看下 removeViewInternal(view)的逻辑。

        private void removeViewInternal(View view) {
            final int index = indexOfChild(view);
            if (index >= 0) {
                removeViewInternal(index, view);
            }
        }
    

    该方法先通过indexOfChild获取View在ViewGroup中的索引index,如果在index>=0的情况下,会调用 removeViewInternal(index, view)方法。

     private void removeViewInternal(int index, View view) {
    
            if (mTransition != null) {
                mTransition.removeChild(this, view);
            }
    
            boolean clearChildFocus = false;
            if (view == mFocused) {
                view.unFocus();
                clearChildFocus = true;
            }
    
            if (view.isAccessibilityFocused()) {
                view.clearAccessibilityFocus();
            }
    
            cancelTouchTarget(view);
            cancelHoverTarget(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);
    
            removeFromArray(index);
    
            if (clearChildFocus) {
                clearChildFocus(view);
                if (!rootViewRequestFocus()) {
                    notifyGlobalFocusCleared(this);
                }
            }
    
            onViewRemoved(view);
    
            if (view.isImportantForAccessibility() && view.getVisibility() != View.GONE) {
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
        }
    

    通过追查,发现在removeFromArray(index)中,会将当前index的view的mParent置为null。

        // This method also sets the child's mParent to null
        private void removeFromArray(int index) {
            final View[] children = mChildren;
            if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
                children[index].mParent = null;
            }
            final int count = mChildrenCount;
            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--;
            }
        }
    

    这个方法主要是将ViewGroup的mChildren中相应index的子View给移除,同时也会把相应的子view的mParent置为null,这样这个子view就可以再次被ViewGroup添加了。这也就是为什么我们在报了"The specified child already has a parent. You must call removeView() on the child's parent first."这个异常以后,需要把调用view.getParent().removeAllViews()就可以了。当然,如果确定view在这个ViewGroup中,也可以调用removeView(View view),只把这个子view移除掉就行。

    总结

    • 在ViewGroup调用addView的时候,会先判断子View的mParent是否为空,不为空则会抛出异常;
    • ViewGroup实现了ViewParent接口,在addView的过程中,会把自己复制给子View的mParent;
    • 在ViewGroup中,调用removeView(),会清除子View的mParent对象;

    本文只是从源码搞清楚了这个异常出现的原因,但是一般我们开发是不应该将一个子view连续add两次的,在特别复杂的布局中,如果真的出现了这个异常,可以尝试调用view.getParent().removew(view)来解决问题,但是这时候也说明你需要梳理你的逻辑是否出现了问题。

    相关文章

      网友评论

          本文标题:源码分析同一个View为什么不能被ViewGroup连续addV

          本文链接:https://www.haomeiwen.com/subject/iafjeftx.html