为什么重复添加子View会报错

作者: 埃赛尔 | 来源:发表于2017-03-29 15:03 被阅读462次

    先从一段异常开始吧,这是在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它是在什么时候报错了呢?
    看这张异常图片

    QQ图片20170329102258.png

    其实这张图片告诉了我们很多事情,比如:
    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所在的线程中(默认是在主线程);

    相关文章

      网友评论

        本文标题:为什么重复添加子View会报错

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