温馨提示
请滑动到文章末尾,长按识别「抽奖」小程序,每日现金红包,月月百元大红包,「加蛋添肉」快到碗里来。
前言
好多年没有被淋成「落汤鸡」了,今天的雨是真的很大,毫不夸张的说,我鞋子里至少装了一斤的雨水。厦门的天气暴雨说来就来,让你猝不及防。
今天的主题是 LinearLayout 和 RelativeLayout 性能对比,你认为谁的效率更高?曾在百度的面试中被问到过。
正文
我们先来做个简单的测试,分别用 LinearLayout 和 RelativeLayout 布局下图样式,打印时长 Measure,Layout,draw 时长。
结论如下:
LinearLayout
Measure:0.738ms
Layout:0.176ms
draw:7.655ms
RelativeLayout
Measure:2.280ms
Layout:0.153ms
draw:7.696ms
数据显示无论使用 RelativeLayout 还是 LinearLayout,layout 和 draw 的过程两者相差无几,考虑到误差的问题,几乎可以认为两者相差无几,关键是 Measure 的过程 RelativeLayout 却比 LinearLayout 慢了一大截。
接下来分析 RelativeLayout 与 LinearLayout 的 Measure 源码。
RelativeLayout的onMeasure相关源码:
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();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
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) {
if (isLayoutRtl()) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, myWidth - params.mLeft);
} else {
width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
}
} else {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, params.mRight);
} else {
width = Math.max(width, params.mRight + params.rightMargin);
}
}
}
if (isWrapContentHeight) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
height = Math.max(height, params.mBottom);
} else {
height = Math.max(height, params.mBottom + params.bottomMargin);
}
}
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 会对子 View 做两次 measure。这是为什么呢?
首先 RelativeLayout 中子 View 的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中 View 的顺序并不相同,在确定每个子 View 的位置的时候,就需要先给所有的子 View 排序一下。
又因为 RelativeLayout 允许 A,B 2 个子 View,横向上 B 依赖 A,纵向上 A 依赖 B。所以需要横向纵向分别进行一次排序测量。
LinearLayout的onMeasure相关源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
与 RelativeLayout 相比 LinearLayout 的 measure 就简单明了的多了,先判断线性规则,然后执行对应方向上的测量。比如这样:
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
父视图在对子视图进行 measure 操作的过程中,使用变量 mTotalLength 保存已经 measure 过的 child 所占用的高度,该变量刚开始时是 0。
在 for 循环中调用 measureChildBeforeLayout 对每一个 child 进行测量,该函数实际上仅仅是调用了 measureChildWithMargins,在调用该方法时,使用了两个参数。
其中一个是heightMeasureSpec,该参数为LinearLayout本身的measureSpec;另一个参数就是 mTotalLength,代表该 LinearLayout已经被其子视图所占用的高度。
每次 for 循环对 child 测量完毕后,调用 child.getMeasuredHeight 获取该子视图最终的高度,并将这个高度添加到 mTotalLength 中。
在本步骤中,暂时避开了 lp.weight>0 的子视图,即暂时先不测量这些子视图,因为后面将把父视图剩余的高度按照 weight 值的大小平均分配给相应的子视图。
源码中使用了一个局部变量 totalWeight 累计所有子视图的 weight 值。处理 lp.weight>0 的情况需要注意,如果变量 heightMode 是 EXACTLY,那么,当其他子视图占满父视图的高度后,weight>0 的子视图可能分配不到布局空间,从而不被显示,只有当heightMode是AT_MOST或者UNSPECIFIED 时,weight>0 的视图才能优先获得布局高度。
最后我们的结论是:如果不使用 weight 属性,LinearLayout 会在当前方向上进行一次 measure 的过程,如果使用 weight 属性,LinearLayout 会避开设置过 weight 属性的 view 做第一次 measure,完了再对设置过weight 属性的 view 做第二次 measure。由此可见,weight 属性对性能是有影响的,请考量是否有更好的方案再决定是否使用。
分析到这里,我们就可以得出以下结论。
结论
RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View2次onMeasure
RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
最后再思考一下文章开头那个矛盾的问题,为什么 Google 给开发者默认新建了个 RelativeLayout,而自己却在 DecorView 中用了个LinearLayout。
因为 DecorView 的层级深度是已知而且固定的,上面一个标题栏,下面一个内容栏。采用 RelativeLayout 并不会降低层级深度,所以此时在根节点上用 LinearLayout 是效率最高的。
而之所以给开发者默认新建了个 RelativeLayout 是希望开发者能采用尽量少的 View 层级来表达布局以实现性能最优,因为复杂的 View 嵌套对性能的影响会更大一些。
推荐阅读:
长按识别小程序,参与抽奖
目前100000+人已关注加入我们