View爲什麼會至少進行2次onMeasure、onLayout

前言

郭前輩的ListView源碼解析一文,曾提到View至少會進行2次onMeasure、onLayout,但限於篇幅,並未解釋原因,好奇就嘗試找了找原因。

原因猜想

5673075-e6af10e313a73939.jpg
害怕.jpg

由於不知道具體原因,只能結合已有的知識,先做出如下猜想:
1.View自身進行了2次onMeasure、onLayout
2.ViewGroup對Child進行了2次measure、layout
3.我們知道View的繪製流程都始於ViewRootImpl的performTraversals方法,有理由懷疑performTranversals執行了2次。
PS:趕時間的朋友,可直接閱讀驗證三

驗證一、二

按照上面的猜想,先進入View類查找總共就2處調用了onMeasure方法如下:

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
 .........
  final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
 .........
  int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
  if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_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;
        }
   ..........
}
 /**
     * Flag indicating that a call to measure() was skipped and should be done
     * instead when layout() is invoked.
     */
    static final int PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT = 0x8;

第二次onMeasure,但是註釋說的很明確,只有當measure方法未被調用的時候,纔會在layout裏面執行一次onMeasure方法,正常的view樹測量流程,每個view的measure方法確實的都被調用過,所以猜想一排除。

關於猜想二,以FrameLayout爲例,確實是在onMeasure方法中對child進行了2次測量,但這是有條件限制的,需要FrameLayout的layout_width/height屬性不能爲match_parent或具體的值,且child的layout屬性必須爲match_parent,具有特殊性,實際上即使不滿足以上條件依舊會進行2次測量,故排除猜想二。

PS:關於一、二的源碼分析,可參考View measure源碼分析

驗證三

看了看代碼,發現會執行2次performTranversals,也就會執行2次測量。
ViewRootImpl#performTraversals()代碼片段一

        //1.由於第一次執行newSurface必定爲true,需要先創建Surface嘛
        //爲true則會執行else語句,所以第一次執行並不會執行 performDraw方法,即View的onDraw方法不會得到調用
        //第二次執行則爲false,並未創建新的Surface,第二次纔會執行 performDraw方法
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }

                performDraw();
            }
        } else {
            //2.viewVisibility是wm.add的那個View的屬性,View的默認值都是可見的
            if (viewVisibility == View.VISIBLE) {
                // Try again
                //3.再執行一次 scheduleTraversals,也就是會再執行一次performTraversals
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }

斷點SDK-23源碼結果如下圖所示:
PS:斷點源碼建議使用模擬器,真機一般都是修改過的,與SDK代碼不一致。

5673075-9eccfbc0b2b3ec1b.png
第一次performTranversals
5673075-566a7c2da15afe4a.png
第二次performTranversals

既然確定了performTravelsals會執行2次,那麼肯定會執行2次measure方法,但是執行2次measure方法就一定會執行2次onMeasure方法嗎?
答案是否定,分析過View measure方法源碼的都應知道measure方法做了2級測量優化:
1.如果flag不爲forceLayout或者與上次測量規格(MeasureSpec)相比未改變,那麼將不會進行重新測量(執行onMeasure方法),直接使用上次的測量值;
2.如果滿足非強制測量的條件,即前後二次測量規格不一致,會先根據目前測量規格生成的key索引緩存數據,索引到就無需進行重新測量;如果targetSDK小於API 20則二級測量優化無效,依舊會重新測量,不會採用緩存測量值。

照理第二次測量應該會取測量的緩存值,並不會重新測量(調用onMeasure)的。然而實際上確重新測量了,那麼極有可能就是第二次performMeasure傳入的測量規格與第一次不同,因爲在layout執行中已經將flag force_layout置爲false了,代碼如下:

  public void layout(int l, int t, int r, int b) {
        .........
        //mPrivateFlags第16位設置爲0,0表示不強制layout
        //PFLAG_FORCE_LAYOUT = 0x00001000
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

按照剛纔的分析,前後二次的傳入的測量規格應該不一致,然而事實是2次傳入onMeasure()的測量規格一致,結果如下:

5673075-d0c39127a3ae2bc0.png
log信息

那麼問題又來了,爲什麼會測量三次呢?首先聲明的是,並不是因爲FrameLayout的多次測量,此處的自定義View並不滿足FrameLayout測量2次child的條件。經過斷點跟蹤SDK源碼發現:
第一次performTranversals會執行2次performMeasure:
先執行measureHierarchy方法中的performMeasure方法

5673075-9c3d802a4a9dbfd4.png
第一次執行處
5673075-d6df57b302bb24e0.png
方法調用棧

接着執行後面的performMeasure,

5673075-34abb38538cf95a1.png
第二次執行處

5673075-8e4072e375a549ff.png
方法調用棧

第二次performTranversals則是隻執行measureHierarchy中的performMeasure方法
這就能解釋爲什麼前2次測量都執行了onMeasure方法,而未採用測量優化策略,因爲前2次performMeasure並未經過performLayout,也即forceLayout的標誌位一直爲true,自然不會取緩存優化。理論上第三次測量經過第一次performTranversals中的performLayout,強制layout的flag應該爲false,然而實際上卻又變成了true,至於在哪兒恢復爲true的,我在源碼中並沒有找到答案。
但是這在api24、25上卻及其符合我們的推論,第三次強制layout的flag爲false,即第二次performTranversals並不會導致View的onMeasure方法的調用,由於未調用onMeasure方法,也不會調用onLayout方法,即api 25只會執行2次onMeasure、一次onLayout、一次onDraw,如下圖所示:

5673075-b0c19d0e5f7d3cdc.png
SDK-24
5673075-5232224e8f135a28.png
SDK-25

總結:

api25-24:執行2次onMeasure、2次onLayout、1次onDraw,理論上執行三次測量,但由於測量優化策略,第三次不會執行onMeasure。
api23-21:執行3次onMeasure、2次onLayout、1次onDraw,forceLayout標誌位,離奇被置爲true,導致無測量優化。
api19-16:執行2次onMeasure、2次onLayout、1次onDraw,原因第一次performTranversals中只會執行measureHierarchy中的performMeasure,forceLayout標誌位,離奇被置位true,導致無測量優化。
總之,造成這個現象的根本原因是performTranversal函數在View的測量流程中會執行2次。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章