??熟悉ConstraintLayout的同學(xué)都知道ConstraintLayout內(nèi)部的子View最少會(huì)measure兩次,一旦內(nèi)部有某些View的measure階段比較耗時(shí)虎忌,那么measure多次就會(huì)把這個(gè)耗時(shí)問題放大婴洼。在我們的項(xiàng)目中栈虚,我們通過Trace信息發(fā)現(xiàn)App的一部分耗時(shí)是因?yàn)檫@個(gè)造成,所以優(yōu)化ConstraintLayout顯得至關(guān)重要。
??最初,我們想到的辦法是替換布局谣妻,將會(huì)measure多次的布局比如說RelativeLayout和ConstraintLayout換成只會(huì)measure一次的FrameLayout,這在一定程度上能夠緩解這個(gè)問題卒稳,但是這樣做畢竟治標(biāo)不治本蹋半。因?yàn)樵谔鎿Q布局過程中,會(huì)發(fā)現(xiàn)很多布局文件根本就換不了充坑,相關(guān)的同學(xué)在開發(fā)過程中選擇其他布局肯定是要使用到其特別的屬性湃窍。那么有沒有一種辦法闻蛀,既能減少原有布局的measure次數(shù),又能保證不影響到其本身的特性呢您市?基于此,我去閱讀了ConstraintLayout相關(guān)源碼役衡,了解其內(nèi)部實(shí)現(xiàn)原理茵休,思考出一種方案,用以減少ConstraintLayout的measure次數(shù)手蝎,進(jìn)而減少measure的耗時(shí)榕莺。
??為啥選擇ConstraintLayout來優(yōu)化,而不是較為簡(jiǎn)單的RelativeLayout呢棵介?那是因?yàn)镃onstraintLayout的使用太為廣泛钉鸯,而且RelativeLayout能夠?qū)崿F(xiàn)的布局,ConstraintLayout都能實(shí)現(xiàn)邮辽;其次唠雕,還有一點(diǎn)點(diǎn)私心,想要學(xué)習(xí)一下ConstraintLayout的內(nèi)部實(shí)現(xiàn)原理吨述。
??特別注意岩睁,本文ConstraintLayout的源碼來自于2.0.4版本
在后續(xù)內(nèi)容之前,大家一定要記住揣云,本文使用的是2.0.4版本的ConstraintLayout捕儒。因?yàn)椴煌姹镜腃onstraintLayout,內(nèi)部實(shí)現(xiàn)不完全相同邓夕,所以最終實(shí)現(xiàn)的細(xì)節(jié)可能不同刘莹。
1. 實(shí)現(xiàn)方案
??我們直接開門見山,來介紹一下整個(gè)方案焚刚,主要分為兩步:
- 自定義ConstraintLayout点弯,重寫onMeasure方法,增加一個(gè)判斷汪榔,減少?zèng)]必要測(cè)量
- 設(shè)置ConstrainLayout的optimizationLevel屬性蒲拉,將其修改為
OPTIMIZATION_GRAPH
和OPTIMIZATION_GRAPH_WRAP
,默認(rèn)值為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;
}
??大家從上面的代碼可以看出來幾點(diǎn):
- 在onMeasure方法中調(diào)用skipMeasure方法雌团,用以判斷是否跳過當(dāng)前Measure。
- 在
skipMeasure
方法中士聪,需要注意兩個(gè)點(diǎn):先是判斷了mDirtyHierarchy
锦援,如果mDirtyHierarchy
為true,那么就不跳過measure剥悟;其次灵寺,遍歷了每個(gè)Child曼库,并且判斷child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)
,如果這個(gè)條件為true略板,那么也不跳過measure毁枯。如果前面兩個(gè)條件都不滿足,那么就繼續(xù)往下判斷是否需要跳過叮称,后面會(huì)詳細(xì)解釋為啥要這么做种玛,這里先不多說。
(2). 設(shè)置optimizationLevel
??設(shè)置optimizationLevel有兩個(gè)方法瓤檐,一是在xml文件中赂韵,通過layout_optimizationLevel
屬性設(shè)置,二是通過setOptimizationLevel
方法設(shè)置挠蛉。至于為啥需要設(shè)置optimizationLevel祭示,下面的內(nèi)容會(huì)有解釋。
??通過如上兩步操作進(jìn)行設(shè)置谴古,然后將布局里面的ConstraintLayout替換成為自定義的ConstraintLayout质涛,就可以讓其內(nèi)部的View measure一次。
??我相信讥电,大家在使用此方案之前蹂窖,內(nèi)心有一個(gè)疑問:這個(gè)會(huì)影響使用ConstraintLayout的原有特性嗎?經(jīng)過我簡(jiǎn)單的測(cè)試恩敌,此方案定義的ConstraintLayout并不影響其常規(guī)屬性瞬测。大家可以在KotlinDemo里面找到詳細(xì)的實(shí)現(xiàn)代碼,參考MyConstraintLayout
的實(shí)現(xiàn)纠炮。
2.揭露原理
??在上面的內(nèi)容當(dāng)中月趟,我們進(jìn)行了兩步操作實(shí)現(xiàn)了measure 一次。那么這兩步為啥要這么做呢恢口?上面沒有解釋孝宗,在這里我將揭露其內(nèi)部原理。
??通過已有的知識(shí)和了解到的ConstraintLayout的實(shí)現(xiàn)耕肩,我們可以知道ConstraintLayout會(huì)measure多次因妇,主要體現(xiàn)在兩個(gè)地方:ViewRootImpl可能會(huì)多次調(diào)用performMeasure
方法,最終會(huì)導(dǎo)致ConstraintLayout的onMeasure方法會(huì)調(diào)用多次猿诸;ConstraintLayout內(nèi)部在measure child的時(shí)候婚被,也有可能導(dǎo)致多次measure。所以梳虽,上面的兩步操作分別解決的這兩個(gè)問題:重寫onMeasure方法是避免它被調(diào)用多次址芯;設(shè)置optimizationLevel是避免child 被measure多次。
??我們來看一下這其中的細(xì)節(jié)。
(1). ConstraintLayout的onMeasure方法
??我們直接來看onMeasure方法的源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 如果當(dāng)前View樹的狀態(tài)是最新的谷炸,也嘗試遍歷每個(gè)child北专,
// 看看每個(gè)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. 經(jīng)過上面的重新判斷旬陡,再來判斷是否舍棄本次的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());
}
??這個(gè)onMeasure方法的實(shí)現(xiàn)拓颓,我將其分為三步:
- 當(dāng)
mDirtyHierarchy
為false時(shí),表示當(dāng)前View 樹已經(jīng)經(jīng)歷過測(cè)量了季惩。但是此時(shí)要從每個(gè)child的isLayoutRequested狀態(tài)來判斷是否需要重新測(cè)量录粱,如果為true,表示當(dāng)前child進(jìn)行了requestLayout操作或者forceLayout操作画拾,所以需要重新測(cè)量。這么看好像沒有毛病菜职,但是為啥我們將isLayoutRequested
修改為child.isLayoutRequested() && !(child.getMeasuredHeight() > 0 && child.getMeasuredWidth() > 0)
呢青抛?這個(gè)要從ConstrainLayout的第一次測(cè)量說起,當(dāng)整個(gè)布局添加到ViewRootImpl上去的時(shí)候酬核,ViewRootImpl會(huì)調(diào)用Constraintlayout的onMeasure方法蜜另。這里有一個(gè)點(diǎn)需要注意的是,在正式layout之前嫡意,onMeasure方法可能會(huì)調(diào)用多次举瑰,同時(shí)isLayoutRequested
會(huì)一直為true,因?yàn)檫@個(gè)狀態(tài)在layout階段才清空的蔬螟。也就是說此迅,在layout之前,盡管mDirtyHierarchy
已經(jīng)為false了旧巾,還是會(huì)重新測(cè)量一遍所有的child耸序。可實(shí)際上鲁猩,此時(shí)child的width和height已經(jīng)確定了坎怪,沒必要在測(cè)量一遍,所以這里我增加了寬高的限制廓握,保證child已經(jīng)measure了搅窿,不會(huì)再measure。- 經(jīng)過第一點(diǎn)的判斷隙券,如果此時(shí)
mDirtyHierarchy
還為false男应,表示當(dāng)前View樹不需要再測(cè)量,因此就直接return即可(實(shí)際上是尔,這里沒有直接return殉了,而是另外做了一些判斷,用以保證measure沒有問題拟枚。)薪铜。我們?cè)诙xskipMeasure方法的時(shí)候众弓,就是這部分的代碼拷貝出來的,用以保證內(nèi)外判斷一致隔箍。- 如果上面兩個(gè)條件都不滿足谓娃,那么就表示需要測(cè)量child,就調(diào)用resolveSystem方法測(cè)量所有的child蜒滩。
??上面的第一點(diǎn)中滨达,我已經(jīng)解釋了為啥我們需要重寫onMeasure方法,目的是為了過濾沒必要的測(cè)量俯艰。那么可能有人要問捡遍,正常的測(cè)量會(huì)被過濾嗎?其實(shí)重點(diǎn)在于mDirtyHierarchy
為false的情況下竹握,會(huì)影響到某些測(cè)量嗎画株?從一個(gè)方面來看,第一次測(cè)量基本沒有什么問題啦辐,還有一種情況就是谓传,動(dòng)態(tài)的修改View的寬高會(huì)有影響嗎?動(dòng)態(tài)修改布局芹关,最終都會(huì)導(dǎo)致requestLayout续挟,然而我們從ConstraintLayout的實(shí)現(xiàn)可以看出來,Google爸爸在requestLayout和forceLayout兩個(gè)方法里面都將mDirtyHierarchy
設(shè)置為true了侥衬,所以理論上不會(huì)造成影響诗祸。
(2). measure child
??從上面的介紹,我們知道ConstraintLayout在measure child浇冰,也有可能measure多次贬媒,我們來看一下為啥會(huì)measure多次。細(xì)節(jié)我們就不分析了肘习,我們直接跳到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) {
// 判斷優(yōu)化是否失效
}
// ······
optimize &= (widthMode == EXACTLY && heightMode == EXACTLY) || optimizeWrap;
int computations = 0;
if (optimize) {
// 如果優(yōu)化生效际乘,那么通過Graph的方式測(cè)量child,這個(gè)過程中只會(huì)measure child 一次。
} else {
// ·······
}
if (!allSolved || computations != 2) {
// 如果沒有優(yōu)化漂佩,或者優(yōu)化的measure沒有完全解決measure脖含,會(huì)兜底測(cè)量
// 這個(gè)過程可能會(huì)有多次measure child
}
if (LinearSystem.MEASURE) {
layoutTime = (System.nanoTime() - layoutTime);
}
return layoutTime;
}
??從這里,我們可以看出來投蝉,只要我們?cè)O(shè)置了optimizationLevel
养葵,就有可能讓所有的child只measure一次,這也是我們想要的結(jié)果瘩缆。而且关拒,就算measure有問題,ConstaintLayout在測(cè)量過程中發(fā)現(xiàn)了問題,即allSolved
為false着绊,也會(huì)進(jìn)行兜底谐算。
3. 總結(jié)
??經(jīng)過上面的介紹,我們基本能理解整個(gè)優(yōu)化ConstraintLayout measure具體內(nèi)容归露,在這里洲脂,我簡(jiǎn)單的做一個(gè)總結(jié)。
- 重寫onMeasure方法是為了保證ConstraintLayout的onMeasure只會(huì)執(zhí)行一次剧包。
- 設(shè)置optimizationLevel恐锦,是為了保證child只會(huì)被measure一次。