从一次诡异的Bug出发,窥探View更新的原理

前言

1 最近业务,有一个复现步骤和路径非常长的bug,经历过一些问题之后,出现名称和其他元素不显示的问题.这个问题复现步骤长,而且多次排查(陆陆续续一个多月,公司所有大佬都来看过没有找到真正原因),并没有什么布局问题,布局都是正常的布局

  1. Debug问题出现点,发现里面的显示名称TextView,有名称时展示,没名称是Gone

    if (TextUtils.isEmpty(name)) {
                mName.setVisibility(GONE);
            } else {
                mName.setVisibility(VISIBLE);
            }
    

    这样理论上来说,不会有什么问题,每次name不为空时,TextView由GONE变为Visible状态,这个时候,会触发TextView发出requestLayout,因为布局发生改变了(Gone不占用空间,而Visible占用空间),而观众上座后,不显示名称,Debug发现TextView 已经Visible了,但是宽高都是0,我们之前 requestLayout必须层层传递,发到最顶级的父类ViewRootImpl中才会有效,说明这个请求没有发出去,导致没有走onLayout,所以自然没有宽高

  2. 顺着上面的思路,看看,一个View发出requestLayout只有,到底走了那些流程

    public void requestLayout() {
            if (mMeasureCache != null) mMeasureCache.clear();
            // 判断当前View是否已经attach了 当前肯定是已经attach了
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
                // Only trigger request-during-layout logic if this is the view requesting it,
                // not the views in its parent hierarchy
                ViewRootImpl viewRoot = getViewRootImpl();
                if (viewRoot != null && viewRoot.isInLayout()) {
                    if (!viewRoot.requestLayoutDuringLayout(this)) {
                        return;
                    }
                }
                mAttachInfo.mViewRequestingLayout = this;
            }
            // 把 mPrivateFlags 改为 PFLAG_FORCE_LAYOUT 说明正在更新布局 
            mPrivateFlags |= PFLAG_FORCE_LAYOUT;
             // 把 mPrivateFlags 改为 PFLAG_INVALIDATED 说明正在重绘  并不会覆盖上面的值 因为采用大bitMap法 32位每个位记录不一样的信息
            mPrivateFlags |= PFLAG_INVALIDATED;
            // isLayoutRequested 父控件是否在更新布局中,如果正在更新布局,则无法响应此次请求
            if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
            if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
                mAttachInfo.mViewRequestingLayout = null;
            }
        }
    

    这里的父控件会一层层的往上传递,直到最顶级的父类ViewRootImpl

    @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
             // 检查是不是主线程
                checkThread();
                //设置标记
                mLayoutRequested = true;
                //真正刷新View树的方法
                scheduleTraversals();
            }
        }
    

    然后

     void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
                // 通过 mChoreographer 发送一个Handler消息,更新布局,每16.5ms更新一次
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
            }
        }
    

    执行了 mTraversalRunnable 这个Runable里面的方法为doTraversal

    void doTraversal() {
                ....
                try {
                    performTraversals();
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
                ...
            }
        }
    

    最终走的是performTraversals

    private void performTraversals() {
        ..... 
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        .....
        performDraw();
        .....
    }
    
    

    喂喂, Google大佬们, 这个方法明显超行了好不好, 一个方法代码200多行,要命了,

    最主要的是调用这三个方法,后面的方法,大家都知道了

    performMeasure -> Measure->onMeasure()-> measureChildren->chlid onMeasure()
    performLayout -> layout -> onLayout
    performDraw -> draw->onDraw

  3. 从上面流程可以看出,要想TextView的OnLayout 执行,必须requestLayout发到底层的ViewRootImpl中,问题的原因是因为requestlayout的请求没有发出去,到底是哪里出了问题, 后续通过一步步的Debug该View的父类,发现有一个WindowControllerView的父类,requestlayout发到他这里,接收了,但是没有往上传递,继续Debug源码,发现

    if (mParent != null && !mParent.isLayoutRequested()) {
                mParent.requestLayout();
            }
    

    mParent.isLayoutRequested()这个返回为true,导致没有执行,查看该方法的实现

    public boolean isLayoutRequested() {
            return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        }
    

    还是这个 mPrivateFlags 的原因,最终定位到这个mPrivateFlags上,就是因为这个mPrivateFlags的状态异常,导致整个 View 树无法得到刷新

  4. 那该标记位什么时候变化,搜索整个源码 发现 Layout Measure Draw focus等方法中会改变,而 requestLayout 中会变为PFLAG_FORCE_LAYOUT 而这个值什么时候可以改变呢?Layout

    public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList listenersCopy =
                            (ArrayList)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
         // 这里 改为了非PFLAG_FORCE_LAYOUT值
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }
    

    所以说,requestLayout和 layout 方法,一一对应,如果只有一个执行,另外一个不执行,都会导致mPrivateFlags状态错误

  5. 根源找到了,接下来就是在该TextView所有的父控件的 requestlayout 和 onlayout 都打上日志,运行发现,重新走复现步骤发现,一个父控件 requestLayout 了 但是没有继续 走 onLayout,所有,真正的问题点就在这里,就是因为这一次的 requestLayout,导致mPrivateFlags错误,

  6. 打印程序堆栈信息,发现是一个 Media层的回调,联想到之前 Media层回调经常忘记切线程,故意打了一个线程 Id,果然,线程 ID 为 thread-2580

  7. 一切理清楚了,在子线程一个 TextView.setText 了,引起了父 View 在子线程 request 了,而 requestLayout 在子线程中根本无法生效,到不了 ViewRootImpl,Layout 方法不走,mPrivateFlags状态一直重置不回来,导致后续的所有 requestlayout 无法生效

  8. 什么,你问为什么在子线程 requestLayout 不会异常,因为 检测线程的代码,全都在ViewRootImpl 源码中, requestLayout 发不出去,自然不会调用检测线程的代码,也自然没有问题

  9. 感谢

requestLayout in layout问题

你可能感兴趣的:(从一次诡异的Bug出发,窥探View更新的原理)