前言
1 最近業(yè)務,有一個復現(xiàn)步驟和路徑非常長的bug,經歷過一些問題之后,出現(xiàn)名稱和其他元素不顯示的問題.這個問題復現(xiàn)步驟長,而且多次排查(陸陸續(xù)續(xù)一個多月,公司所有大佬都來看過沒有找到真正原因),并沒有什么布局問題,布局都是正常的布局
-
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,所以自然沒有寬高 -
順著上面的思路,看看,一個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里面的方法為doTraversalvoid doTraversal() { .... try { performTraversals(); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } ... } }最終走的是
performTraversalsprivate void performTraversals() { ..... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ...... performLayout(lp, desiredWindowWidth, desiredWindowHeight); ..... performDraw(); ..... }喂喂, Google大佬們, 這個方法明顯超行了好不好, 一個方法代碼2000多行,要命了,
最主要的是調用這三個方法,后面的方法,大家都知道了
performMeasure -> Measure->onMeasure()-> measureChildren->chlid onMeasure()
performLayout -> layout -> onLayout
performDraw -> draw->onDraw
-
從上面流程可以看出,要想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 樹無法得到刷新 -
那該標記位什么時候變化,搜索整個源碼 發(fā)現(xiàn)
LayoutMeasureDrawfocus等方法中會改變,而 requestLayout 中會變?yōu)?code>PFLAG_FORCE_LAYOUT 而這個值什么時候可以改變呢?Layoutpublic 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)錯誤 根源找到了,接下來就是在該TextView所有的父控件的 requestlayout 和 onlayout 都打上日志,運行發(fā)現(xiàn),重新走復現(xiàn)步驟發(fā)現(xiàn),一個父控件 requestLayout 了 但是沒有繼續(xù) 走 onLayout,所有,真正的問題點就在這里,就是因為這一次的 requestLayout,導致
mPrivateFlags錯誤,打印程序堆棧信息,發(fā)現(xiàn)是一個 Media層的回調,聯(lián)想到之前 Media層回調經常忘記切線程,故意打了一個線程 Id,果然,線程 ID 為 thread-2580
一切理清楚了,在子線程一個 TextView.setText 了,引起了父 View 在子線程 request 了,而 requestLayout 在子線程中根本無法生效,到不了 ViewRootImpl,Layout 方法不走,
mPrivateFlags狀態(tài)一直重置不回來,導致后續(xù)的所有 requestlayout 無法生效什么,你問為什么在子線程 requestLayout 不會異常,因為 檢測線程的代碼,全都在ViewRootImpl 源碼中, requestLayout 發(fā)不出去,自然不會調用檢測線程的代碼,也自然沒有問題
感謝