前言
关于页面的性能如何优化,可能刚开始工作时,只知道减少层级或者使用ViewStub懒加载控件等方式来优化。如果要做更加深入的优化怎么办呢?(层级已经减少到尽量最低了,在初始化不需要显示的布局也使用了懒加载的方式了),那么我们可能需要具体去对每一个View的使用进行深度的分析了!
来看一个现象:用Android studio,新建一个Activity自动生成的布局文件之前都是RelativeLayout(当然现在默认是ConstraintLayout),当然这是SDK有意为之的。为什么呢?当然是这样性能更好咯,性能至上嘛。但是当我们去查看content的顶级View,也就是DecoreView时,发现它的内部是一个LinearLayout,上面是标题栏,下面是内容栏。那么问题来了,Google为什么给开发者默认新建了个RelativeLayout/ConstraintLayout,而自己却偷偷用LinearLayout,到底谁的性能更高呢?所以引出了本篇对RelativeLayout、LinearLayout、FrameLayout的性能分析。
分析之前需要知道一些View的基本问题。
View是什么?
View是Android系统在屏幕上的呈现的一种表现形式,也就是说你在屏幕上能看到的东西都是View。
View是怎么绘制出来的?
View的绘制流程是从ViewRootImpl的performTraversals()方法开始,依次经过measure(),layout()和draw()三个过程,各自方法进行深度遍历。才最终将一个View绘制出来。
View是怎么呈现在界面上的?
Android中的视图都是通过Window来呈现的,不管Activity、Dialog还是Toast它们都有一个Window,然后通过WindowManager来管理View。Window和顶级View(DecorView)的通信是依赖ViewRootImpl完成的。
View和ViewGroup什么区别?
不管简单的Button和TextView还是复杂的RelativeLayout和ListView,他们的共同基类都是View。所以说,View是一种界面层控件的抽象,他代表了一个控件。那ViewGroup是什么东西,它可以被翻译成控件组,即一组View。ViewGroup也是继承View,这就意味着View本身可以是单个控件,也可以是多个控件组成的控件组。根据这个理论,Button显然是个View,而RelativeLayout不但是一个View还可以是一个ViewGroup,而ViewGroup内部是可以有子View的,这个子View同样也可能是ViewGroup,以此类推。
RelativeLayout与LinearLayout性能PK
当RelativeLayout和LinearLayout分别作为ViewGroup,表达相同布局时绘制在屏幕上时谁更快一点。上面已经简单说了View的绘制,从ViewRoot的performTraversals()方法开始依次调用perfromMeasure、performLayout和performDraw这三个方法。这三个方法分别完成顶级View的measure、layout和draw三大流程,其中perfromMeasure会调用measure,measure又会调用onMeasure,在onMeasure方法中则会对所有子元素进行measure,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程,接着子元素会重复父容器的measure,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw也分别完成perfromMeasure类似的流程。通过这三大流程,分别遍历整棵View树,就实现了Measure,Layout,Draw这一过程,View就绘制出来了。所以接下来我们可以跟踪一下RelativeLayout和LinearLayout这三大流程的执行耗时。我们可以构建一个相同的布局,来比对谁更耗时:
分别用LinearLayout和RelativeLayout来作为父布局。
public class MyRelativeLayout extends RelativeLayout {
private static final String TAG = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
setWillNotDraw(false);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
long s = System.nanoTime();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i(TAG, "onMeasure time: "+(System.nanoTime() - s));
Log.i(TAG, "onMeasure: ");
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
long s = System.nanoTime();
super.onLayout(changed, l, t, r, b);
Log.i(TAG, "onLayout time: "+(System.nanoTime() - s));
Log.i(TAG, "onLayout: ");
}
@Override
protected void onDraw(Canvas canvas) {
long s = System.nanoTime();
super.onDraw(canvas);
Log.i(TAG, "onDraw time: "+(System.nanoTime() - s));
Log.i(TAG, "onDraw: ");
}
}
LinearLayout的代码也是同理。
LinearLayout
Measure:4617769 (4.617ms)
Layout:1005308 (1.005ms)
draw:56461 (0.056ms)
RelativeLayout
Measure:1947154 (1.947ms)
Layout:993924 (0.994ms)
draw:62923 (0.063ms)
从打印的结果来看,无论使用RelativeLayout还是LinearLayout,layout和draw的过程两者相差无几,考虑到误差的问题,可以认为两者耗时相同,关键是Measure的过程RelativeLayout却比LinearLayout慢了一大截。
所以,这里我们可以分析一下Measure里面到底干了什么?
RelativeLayout的onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
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;
}
}
}
...
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight, child.getBaseline());
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);
}
}
}
...
setMeasuredDimension(width, height);
}
根据源码我们发现RelativeLayout会对子View做两次measure。why?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 两个子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);
}
}
LinearLayout的onMeasure比较简单,先判断orientation,排列方向,然后根据对应方向完成一次测量即可!
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
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;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
...
if (skippedMeasure
|| ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
if (childWeight > 0) {
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
childHeight = share;
} else {
childHeight = child.getMeasuredHeight() + share;
}
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
}
...
}
LinearLayout在对子View进行measure操作的过程中,使用变量mTotalLength保存已经测量过的child所占用的高度,该变量默认是0。在for循环中调用measureChildBeforeLayout方法对每一个child进行测量。 每次for循环对child测量完毕后,调用child.getMeasuredHeight()获取该子视图最终的高度,并将这个高度添加到mTotalLength中。接下来处理lp.weight>0的情况需要注意,如果变量heightMode是EXACTLY,那么,当其他子视图占满父视图的高度后,weight>0的子视图分配不到布局空间,不被显示,只有当heightMode是AT_MOST或UNSPECIFIED时,weight>0的视图才能优先获得布局高度。
总结:如果不使用weight属性,LinearLayout会在mOrientation 指定的方向上进行一次measure的过程,如果使用weight属性,LinearLayout会先过滤设置过weight属性的view做一次measure,之后再对设置过weight属性的view做一次measure。由此可见,weight属性对性能是有一定的影响,
总结RelativeLayout和LinearLayout:之前测试的数据结果之所以RelativeLayout的measure耗时会是LinearLayout的两倍左右的原因是,RelativeLayout会对子view进行两次measure。而LinearLayout只需要一次。但是LinearLayout中如果存在layout_weight属性,也会有第二次view的测量,但是性能仍会比RelativeLayout好。
FrameLayout与LinearLayout性能PK
这次布局中间只放一个居中的view,代码较简单,就不贴出来了。
LinearLayout
Measure:356923(0.357ms)
Layout:399307(0.399ms)
draw:56153 (0.056ms)
FrameLayout
Measure:285231(0.285ms)
Layout:349308(0.349ms)
draw:53528 (0.053ms)
似乎没有数据较大的过程
FrameLayout的onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
代码中可以看到会对各个子view进行一次measure,如果FrameLayout宽或高是EXACTLY 模式,子view又是MATCH_PARENT模式的话,会进行第二次measure。这里我设置的布局不存在这样的情况。猜想如果给FrameLayout设置一个固定宽高,子view设置MATCH_PARENT的话,measure耗时会增加~
结论
1.RelativeLayout会让子View调用两次onMeasure,LinearLayout 在有weight时,也会调用子View两次onMeasure
2.在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
3.能用两层LinearLayout,尽量用一个RelativeLayout,在时间上此时RelativeLayout耗时更小。另外LinearLayout慎用layout_weight,否则会增加耗时。总之减少层级结构,才是王道,让onMeasure做延迟加载,用viewStub,include等也是一种优化手段。
个人理解,如有错误,欢迎指出~