??熟悉ConstraintLayout的同學都知道ConstraintLayout內部的子View最少會measure兩次,一旦內部有某些View的measure階段比較耗時,那么measure多次就會把這個耗時問題放大。在我們的項目中,我們通過Trace信息發現App的一部分耗時是因為這個造成,所以優化ConstraintLayout顯得至關重要。
??最初,我們想到的辦法是替換布局,將會measure多次的布局比如說RelativeLayout和ConstraintLayout換成只會measure一次的FrameLayout,這在一定程度上能夠緩解這個問題,但是這樣做畢竟治標不治本。因為在替換布局過程中,會發現很多布局文件根本就換不了,相關的同學在開發過程中選擇其他布局肯定是要使用到其特別的屬性。那么有沒有一種辦法,既能減少原有布局的measure次數,又能保證不影響到其本身的特性呢?基于此,我去閱讀了ConstraintLayout相關源碼,了解其內部實現原理,思考出一種方案,用以減少ConstraintLayout的measure次數,進而減少measure的耗時。
??為啥選擇ConstraintLayout來優化,而不是較為簡單的RelativeLayout呢?那是因為ConstraintLayout的使用太為廣泛,而且RelativeLayout能夠實現的布局,ConstraintLayout都能實現;其次,還有一點點私心,想要學習一下ConstraintLayout的內部實現原理。
??特別注意,本文ConstraintLayout的源碼來自于2.0.4版本
在后續內容之前,大家一定要記住,本文使用的是2.0.4版本的ConstraintLayout。因為不同版本的ConstraintLayout,內部實現不完全相同,所以最終實現的細節可能不同。
1. 實現方案
??我們直接開門見山,來介紹一下整個方案,主要分為兩步:
- 自定義ConstraintLayout,重寫onMeasure方法,增加一個判斷,減少沒必要測量
- 設置ConstrainLayout的optimizationLevel屬性,將其修改為
OPTIMIZATION_GRAPH
和OPTIMIZATION_GRAPH_WRAP
,默認值為OPTIMIZATION_DIRECT
(1). 重寫onMeasure
??我直接貼代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mMeasureOpt && skipMeasure(widthMeasureSpec, heightMeasureSpec)) {
return;
}
mOnMeasureWidthMeasureSpec = widthMeasureSpec;
mOnMeasureHeightMeasureSpec = heightMeasureSpec;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 用以判斷是否跳過本次Measure。
*/
private boolean skipMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
return false;
}
final int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
View child = getChildAt(index);
if (child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)) {
return false;
}
}
if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && mOnMeasureHeightMeasureSpec == heightMeasureSpec) {
resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(), mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
return true;
}
if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST && MeasureSpec.getMode(mOnMeasureHeightMeasureSpec) == MeasureSpec.AT_MOST) {
int newSize = MeasureSpec.getSize(heightMeasureSpec);
if (newSize >= mLayoutWidget.getHeight()) {
mOnMeasureWidthMeasureSpec = widthMeasureSpec;
mOnMeasureHeightMeasureSpec = heightMeasureSpec;
resolveMeasuredDimension(
widthMeasureSpec,
heightMeasureSpec,
mLayoutWidget.getWidth(),
mLayoutWidget.getHeight(),
mLayoutWidget.isWidthMeasuredTooSmall(),
mLayoutWidget.isHeightMeasuredTooSmall()
);
return true;
}
}
return false;
}
??大家從上面的代碼可以看出來幾點:
- 在onMeasure方法中調用skipMeasure方法,用以判斷是否跳過當前Measure。
- 在
skipMeasure
方法中,需要注意兩個點:先是判斷了mDirtyHierarchy
,如果mDirtyHierarchy
為true,那么就不跳過measure;其次,遍歷了每個Child,并且判斷child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)
,如果這個條件為true,那么也不跳過measure。如果前面兩個條件都不滿足,那么就繼續往下判斷是否需要跳過,后面會詳細解釋為啥要這么做,這里先不多說。
(2). 設置optimizationLevel
??設置optimizationLevel有兩個方法,一是在xml文件中,通過layout_optimizationLevel
屬性設置,二是通過setOptimizationLevel
方法設置。至于為啥需要設置optimizationLevel,下面的內容會有解釋。
??通過如上兩步操作進行設置,然后將布局里面的ConstraintLayout替換成為自定義的ConstraintLayout,就可以讓其內部的View measure一次。
??我相信,大家在使用此方案之前,內心有一個疑問:這個會影響使用ConstraintLayout的原有特性嗎?經過我簡單的測試,此方案定義的ConstraintLayout并不影響其常規屬性。大家可以在KotlinDemo里面找到詳細的實現代碼,參考MyConstraintLayout
的實現。
2.揭露原理
??在上面的內容當中,我們進行了兩步操作實現了measure 一次。那么這兩步為啥要這么做呢?上面沒有解釋,在這里我將揭露其內部原理。
??通過已有的知識和了解到的ConstraintLayout的實現,我們可以知道ConstraintLayout會measure多次,主要體現在兩個地方:ViewRootImpl可能會多次調用performMeasure
方法,最終會導致ConstraintLayout的onMeasure方法會調用多次;ConstraintLayout內部在measure child的時候,也有可能導致多次measure。所以,上面的兩步操作分別解決的這兩個問題:重寫onMeasure方法是避免它被調用多次;設置optimizationLevel是避免child 被measure多次。
??我們來看一下這其中的細節。
(1). ConstraintLayout的onMeasure方法
??我們直接來看onMeasure方法的源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 如果當前View樹的狀態是最新的,也嘗試遍歷每個child,
// 看看每個child是否重新layout。
if (!mDirtyHierarchy) {
// it's possible that, if we are already marked for a relayout, a view would not call to request a layout;
// in that case we'd miss updating the hierarchy correctly.
// We have to iterate on our children to verify that none set a request layout flag...
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.isLayoutRequested()) {
mDirtyHierarchy = true;
break;
}
}
}
// 3. 經過上面的重新判斷,再來判斷是否舍棄本次的measure(不measure child就理解為舍棄本次measure)
if (!mDirtyHierarchy) {
if (mOnMeasureWidthMeasureSpec == widthMeasureSpec && mOnMeasureHeightMeasureSpec == heightMeasureSpec) {
resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
return;
}
if (mOnMeasureWidthMeasureSpec == widthMeasureSpec
&& MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST
&& MeasureSpec.getMode(mOnMeasureHeightMeasureSpec) == MeasureSpec.AT_MOST) {
int newSize = MeasureSpec.getSize(heightMeasureSpec);
if (DEBUG) {
System.out.println("### COMPATIBLE REQ " + newSize + " >= ? " + mLayoutWidget.getHeight());
}
if (newSize >= mLayoutWidget.getHeight()) {
mOnMeasureWidthMeasureSpec = widthMeasureSpec;
mOnMeasureHeightMeasureSpec = heightMeasureSpec;
resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
return;
}
}
}
mOnMeasureWidthMeasureSpec = widthMeasureSpec;
mOnMeasureHeightMeasureSpec = heightMeasureSpec;
mLayoutWidget.setRtl(isRtl());
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
if (updateHierarchy()) {
mLayoutWidget.updateHierarchy();
}
}
// 3. measure child
resolveSystem(mLayoutWidget, mOptimizationLevel, widthMeasureSpec, heightMeasureSpec);
resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, mLayoutWidget.getWidth(), mLayoutWidget.getHeight(),
mLayoutWidget.isWidthMeasuredTooSmall(), mLayoutWidget.isHeightMeasuredTooSmall());
}
??這個onMeasure方法的實現,我將其分為三步:
- 當
mDirtyHierarchy
為false時,表示當前View 樹已經經歷過測量了。但是此時要從每個child的isLayoutRequested狀態來判斷是否需要重新測量,如果為true,表示當前child進行了requestLayout操作或者forceLayout操作,所以需要重新測量。這么看好像沒有毛病,但是為啥我們將isLayoutRequested
修改為child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)
呢?這個要從ConstrainLayout的第一次測量說起,當整個布局添加到ViewRootImpl上去的時候,ViewRootImpl會調用Constraintlayout的onMeasure方法。這里有一個點需要注意的是,在正式layout之前,onMeasure方法可能會調用多次,同時isLayoutRequested
會一直為true,因為這個狀態在layout階段才清空的。也就是說,在layout之前,盡管mDirtyHierarchy
已經為false了,還是會重新測量一遍所有的child??蓪嶋H上,此時child的width和height已經確定了,沒必要在測量一遍,所以這里我增加了寬高的限制,保證child已經measure了,不會再measure。- 經過第一點的判斷,如果此時
mDirtyHierarchy
還為false,表示當前View樹不需要再測量,因此就直接return即可(實際上,這里沒有直接return,而是另外做了一些判斷,用以保證measure沒有問題。)。我們在定義skipMeasure方法的時候,就是這部分的代碼拷貝出來的,用以保證內外判斷一致。- 如果上面兩個條件都不滿足,那么就表示需要測量child,就調用resolveSystem方法測量所有的child。
??上面的第一點中,我已經解釋了為啥我們需要重寫onMeasure方法,目的是為了過濾沒必要的測量。那么可能有人要問,正常的測量會被過濾嗎?其實重點在于mDirtyHierarchy
為false的情況下,會影響到某些測量嗎?從一個方面來看,第一次測量基本沒有什么問題,還有一種情況就是,動態的修改View的寬高會有影響嗎?動態修改布局,最終都會導致requestLayout,然而我們從ConstraintLayout的實現可以看出來,Google爸爸在requestLayout和forceLayout兩個方法里面都將mDirtyHierarchy
設置為true了,所以理論上不會造成影響。
(2). measure child
??從上面的介紹,我們知道ConstraintLayout在measure child,也有可能measure多次,我們來看一下為啥會measure多次。細節我們就不分析了,我們直接跳到measure child的地方--BasicMeasure的solverMeasure方法里面:
public long solverMeasure(ConstraintWidgetContainer layout,
int optimizationLevel,
int paddingX, int paddingY,
int widthMode, int widthSize,
int heightMode, int heightSize,
int lastMeasureWidth,
int lastMeasureHeight) {
// ······
boolean optimizeWrap = Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH_WRAP);
boolean optimize = optimizeWrap || Optimizer.enabled(optimizationLevel, Optimizer.OPTIMIZATION_GRAPH);
if (optimize) {
// 判斷優化是否失效
}
// ······
optimize &= (widthMode == EXACTLY && heightMode == EXACTLY) || optimizeWrap;
int computations = 0;
if (optimize) {
// 如果優化生效,那么通過Graph的方式測量child,這個過程中只會measure child 一次。
} else {
// ·······
}
if (!allSolved || computations != 2) {
// 如果沒有優化,或者優化的measure沒有完全解決measure,會兜底測量
// 這個過程可能會有多次measure child
}
if (LinearSystem.MEASURE) {
layoutTime = (System.nanoTime() - layoutTime);
}
return layoutTime;
}
??從這里,我們可以看出來,只要我們設置了optimizationLevel
,就有可能讓所有的child只measure一次,這也是我們想要的結果。而且,就算measure有問題,ConstaintLayout在測量過程中發現了問題,即allSolved
為false,也會進行兜底。
3. 總結
??經過上面的介紹,我們基本能理解整個優化ConstraintLayout measure具體內容,在這里,我簡單的做一個總結。
- 重寫onMeasure方法是為了保證ConstraintLayout的onMeasure只會執行一次。
- 設置optimizationLevel,是為了保證child只會被measure一次。