從一次詭異的Bug出發(fā),窺探View更新的原理

前言

1 最近業(yè)務,有一個復現(xiàn)步驟和路徑非常長的bug,經歷過一些問題之后,出現(xiàn)名稱和其他元素不顯示的問題.這個問題復現(xiàn)步驟長,而且多次排查(陸陸續(xù)續(xù)一個多月,公司所有大佬都來看過沒有找到真正原因),并沒有什么布局問題,布局都是正常的布局

  1. Debug問題出現(xiàn)點,發(fā)現(xiàn)里面的顯示名稱TextView,有名稱時展示,沒名稱是Gone

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

    這樣理論上來說,不會有什么問題,每次name不為空時,TextView由GONE變?yōu)閂isible狀態(tài),這個時候,會觸發(fā)TextView發(fā)出requestLayout,因為布局發(fā)生改變了(Gone不占用空間,而Visible占用空間),而觀眾上座后,不顯示名稱,Debug發(fā)現(xiàn)TextView 已經Visible了,但是寬高都是0,我們之前 requestLayout必須層層傳遞,發(fā)到最頂級的父類ViewRootImpl中才會有效,說明這個請求沒有發(fā)出去,導致沒有走onLayout,所以自然沒有寬高

  2. 順著上面的思路,看看,一個View發(fā)出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 發(fā)送一個Handler消息,更新布局,每16.5ms更新一次
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
            }
        }
    

    執(zhí)行了 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大佬們, 這個方法明顯超行了好不好, 一個方法代碼2000多行,要命了,

    最主要的是調用這三個方法,后面的方法,大家都知道了

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

  1. 從上面流程可以看出,要想TextView的OnLayout 執(zhí)行,必須requestLayout發(fā)到底層的ViewRootImpl中,問題的原因是因為requestlayout的請求沒有發(fā)出去,到底是哪里出了問題, 后續(xù)通過一步步的Debug該View的父類,發(fā)現(xiàn)有一個WindowControllerView的父類,requestlayout發(fā)到他這里,接收了,但是沒有往上傳遞,繼續(xù)Debug源碼,發(fā)現(xiàn)

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

    mParent.isLayoutRequested()這個返回為true,導致沒有執(zhí)行,查看該方法的實現(xiàn)

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

    還是這個 mPrivateFlags 的原因,最終定位到這個mPrivateFlags上,就是因為這個mPrivateFlags的狀態(tài)異常,導致整個 View 樹無法得到刷新

  2. 那該標記位什么時候變化,搜索整個源碼 發(fā)現(xiàn) Layout Measure Draw focus等方法中會改變,而 requestLayout 中會變?yōu)?code>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<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)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 方法,一一對應,如果只有一個執(zhí)行,另外一個不執(zhí)行,都會導致mPrivateFlags狀態(tài)錯誤

  3. 根源找到了,接下來就是在該TextView所有的父控件的 requestlayout 和 onlayout 都打上日志,運行發(fā)現(xiàn),重新走復現(xiàn)步驟發(fā)現(xiàn),一個父控件 requestLayout 了 但是沒有繼續(xù) 走 onLayout,所有,真正的問題點就在這里,就是因為這一次的 requestLayout,導致mPrivateFlags錯誤,

  4. 打印程序堆棧信息,發(fā)現(xiàn)是一個 Media層的回調,聯(lián)想到之前 Media層回調經常忘記切線程,故意打了一個線程 Id,果然,線程 ID 為 thread-2580

  5. 一切理清楚了,在子線程一個 TextView.setText 了,引起了父 View 在子線程 request 了,而 requestLayout 在子線程中根本無法生效,到不了 ViewRootImpl,Layout 方法不走,mPrivateFlags狀態(tài)一直重置不回來,導致后續(xù)的所有 requestlayout 無法生效

  6. 什么,你問為什么在子線程 requestLayout 不會異常,因為 檢測線程的代碼,全都在ViewRootImpl 源碼中, requestLayout 發(fā)不出去,自然不會調用檢測線程的代碼,也自然沒有問題

  7. 感謝

requestLayout in layout問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容