前言
郭前輩的ListView源碼解析一文,曾提到View至少會進行2次onMeasure、onLayout,但限於篇幅,並未解釋原因,好奇就嘗試找了找原因。
原因猜想
由於不知道具體原因,只能結合已有的知識,先做出如下猜想:
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代碼不一致。
既然確定了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()的測量規格一致,結果如下:
那麼問題又來了,爲什麼會測量三次呢?首先聲明的是,並不是因爲FrameLayout的多次測量,此處的自定義View並不滿足FrameLayout測量2次child的條件。經過斷點跟蹤SDK源碼發現:
第一次performTranversals會執行2次performMeasure:
先執行measureHierarchy方法中的performMeasure方法
接着執行後面的performMeasure,
第二次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,如下圖所示:
總結:
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次。