0. 前言
我們都知道新建一個Android項目自動生成的Xml布局文件的根節點默認是RelativeLayout,這不是IDE默認設置,而是由android-sdk\tools\templates\activities\EmptyActivity\root\res\layout\activity_simple.xml.ftl這個文件事先就定好了的,在我們的理解里貌似LinearLayout的性能是要比RelativeLayout更優的,那SDK為什么會默認給開發者新建一個RelativeLayout呢?
同時作為頂級View的DecorView卻是個垂直方向的LinearLayout,上面是標題欄,下面是內容欄,我們常用的setContentView()方法就是給內容欄設置布局。那么LinearLayout和RelativeLayout誰的性能更高呢,好吧其實我們都知道前者性能更高,那原因是什么呢?
1. 性能對比
問題的核心在於,當RelativeLayout和LinearLayout分別作為ViewGroup表達相同布局時誰的繪制過程更快一點。
Hierarchy Viewer是隨Android SDK發布的工具,位於Android SDK/tools/hierarchyviewer.bat,使用它可以來檢測View繪制的三大過程的耗時。如果不清楚View繪制的三大過程的,可以參考我之前寫過的 Android開發——View繪制過程源碼解析中詳細的介紹過了,這里就不再贅述了。
通過網上的很多實驗結果我們得之,兩者繪制同樣的界面時layout和draw的過程時間消耗相差無幾,關鍵在於measure過程RelativeLayout比LinearLayout慢了一些。我們知道ViewGroup是沒有onMeasure方法的,這個方法是交給子類自己實現的。因為不同的ViewGroup子類布局都不一樣,那么onMeasure索性就全部交給他們自己實現好了。
所以我們就分別來追蹤下RelativeLayout和LinearLayout的onMeasure過程來探索耗時問題的根源。本文原創,為保證錯誤及時更新請認准原創鏈接SEU_Calvin的博客。
1.1 RelativeLayout的onMeasure分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//...
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyHorizontalSizeRules(params, myWidth);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight);
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
width = Math.max(width, params.mRight);
}
if (isWrapContentHeight) {
height = Math.max(height, params.mBottom);
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
//...
}
根據源碼我們發現RelativeLayout會根據2次排列的結果對子View各做一次measure。這是為什么呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關系,而這個依賴關系可能和Xml布局中View的順序不同,在確定每個子View的位置的時候,需要先給所有的子View排序一下。又因為RelativeLayout允許ViewB在橫向上依賴ViewA,ViewA在縱向上依賴B。所以需要橫向縱向分別進行一次排序測量。
同時需要注意的是View.measure()方法存在以下優化:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
...
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
即如果我們或者我們的子View沒有要求強制刷新,而父View給子View傳入的值也沒有變化(也就是說子View的位置沒變化),就不會做無謂的測量。RelativeLayout在onMeasure中做橫向測量時,縱向的測量結果尚未完成,只好暫時使用myHeight傳入子View系統。這樣會導致在子View的高度和RelativeLayout的高度不相同時(設置了Margin),上述優化會失效,在View系統足夠復雜時,效率問題就會很明顯。
1.2 LinearLayout的onMeasure過程
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
//LinearLayout會先做一個簡單橫縱方向判斷,我們選擇縱向這種情況繼續分析
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//... child為空、Gone以及分界線的情況略去
//累計權重
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//計算
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
//精確模式的情況下,子控件layout_height=0dp且weight大於0無法計算子控件的高度
//但是可以先把margin值合入到總值中,后面根據剩余空間及權值再重新計算對應的高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
if (lp.height == 0 && lp.weight > 0) {
//如果這個條件成立,就代表 heightMode不是精確測量以及wrap_conent模式
//也就是說布局是越小越好,你還想利用權值多分剩余空間是不可能的,只設為wrap_content模式
lp.height = LayoutParams.WRAP_CONTENT;
}
// 子控件測量
measureChildBeforeLayout(child, i, widthMeasureSpec,0, heightMeasureSpec,totalWeight== 0 ? mTotalLength :0);
//獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
//...
}
源碼中已經標注了一些注釋,需要注意的是在每次對child測量完畢后,都會調用child.getMeasuredHeight()獲取該子視圖最終的高度,並將這個高度添加到mTotalLength中。但是getMeasuredHeight暫時避開了lp.weight>0的子View,因為后面會將把剩余高度按weight分配給相應的子View。因此可以得出以下結論:
(1)如果我們在LinearLayout中不使用weight屬性,將只進行一次measure的過程。
(2)如果使用了weight屬性,LinearLayout在第一次測量時避開設置過weight屬性的子View,之后再對它們做第二次measure。由此可見,weight屬性對性能是有影響的。
3. 總結論
(1)RelativeLayout慢於LinearLayout是因為它會讓子View調用2次measure過程,而后者只需一次,但是有weight屬性存在時,后者同樣會進行兩次measure。
(2)RelativeLayout的子View如果高度和RelativeLayout不同,會引發效率問題,可以使用padding代替margin以優化此問題。
(3)在不響應層級深度的情況下,使用Linearlayout而不是RelativeLayout。
結論中的第三條也解釋了文章前言中的問題:DecorView的層級深度已知且固定的,上面一個標題欄,下面一個內容欄,采用RelativeLayout並不會降低層級深度,因此這種情況下使用LinearLayout效率更高。
而為開發者默認新建RelativeLayout是希望開發者能采用盡量少的View層級,很多效果是需要多層LinearLayout的嵌套,這必然不如一層的RelativeLayout性能更好。因此我們應該盡量減少布局嵌套,減少層級結構,使用比如viewStub,include等技巧。可以進行較大的布局優化。具體技巧的使用后面會繼續寫文總結。
最后希望大家多多點贊支持~

