19、View的测量与布局优化:避免过度测量、优化onMeasure和onLayout、使用ConstraintLayout减少嵌套
说到View的测量和布局,我脑子里立刻浮现出几年前的一个深夜。那时候我刚接手一个电商App,首页的Feed流卡顿得让人抓狂。滚动时掉帧严重,帧率经常掉到20fps以下。我打开Systrace一看,好家伙,measure和layout阶段占了整整60%的绘制时间。说白了,就是布局嵌套太深,测量被反复触发了。
今天我们就来聊聊怎么解决这类问题。你想想看,一个View从创建到显示在屏幕上,经历了measure、layout、draw三个阶段。其中measure和layout是决定性能的关键。如果这两个阶段处理不好,后面draw得再快也没用。
19.1 过度测量:性能的隐形杀手
什么是过度测量?我举个例子你就明白了。
假设你有一个RelativeLayout,里面放了三个TextView。每次调用requestLayout(),这个RelativeLayout会重新测量自己,然后通知所有子View重新测量。如果某个子View又触发了父布局的重新测量,那就形成了测量风暴。
我在项目中遇到过最夸张的情况:一个五层嵌套的布局,一次布局变更触发了37次重复测量。你能想象吗?本来一次测量就能搞定的事,硬生生被放大了几十倍。
核心原则:尽量减少measure和layout的调用次数。一次有效的测量,胜过十次无效的重复。
为什么会造成过度测量?主要有三个原因:
- 布局嵌套过深:每层父布局都会触发子布局的测量
- 使用wrap_content不当:wrap_content会触发两次测量(先测量一次确定大小,再根据子View调整)
- 频繁调用requestLayout():每次调用都会触发整棵View树的重新测量
19.2 优化onMeasure:少做无用功
onMeasure是View测量的核心方法。我见过很多开发者在这个方法里做了大量计算,其实很多都是不必要的。
先看一个反面教材:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 错误做法:每次都重新计算所有子View
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 这里做了大量重复计算
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
// 然后又重新计算一遍
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
这段代码的问题在于:手动测量了子View,又调用了super.onMeasure(),导致子View被测量了两次。嗯,这里要注意,不要重复测量。
正确的做法是这样的:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先判断是否需要重新测量
if (getChildCount() == 0) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
return;
}
// 只测量一次,交给super处理
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果需要对测量结果做微调,在这里处理
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
// 比如:确保最小高度
if (measuredHeight < getSuggestedMinimumHeight()) {
measuredHeight = getSuggestedMinimumHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
我的个人习惯:在onMeasure里尽量不做耗时计算。如果确实需要计算,把结果缓存起来。我曾经把一个自定义View的测量时间从8ms降到了0.5ms,就是靠缓存。
19.3 优化onLayout:减少布局传递
onLayout负责给子View分配位置。这里最常见的坑是:在onLayout里触发了requestLayout()。
我曾经遇到过一个Bug:某个自定义ViewGroup在onLayout里根据子View的位置动态调整自身大小,结果调整后又触发了requestLayout(),导致无限循环。最后界面直接卡死了。
优化onLayout的几个要点:
- 避免在onLayout中修改布局参数:如果需要修改,使用post()延迟执行
- 减少子View的遍历次数:一次遍历完成所有子View的布局
- 使用局部布局:如果只有部分子View位置变了,不要重新布局所有子View
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 优化:只布局可见的子View
int childCount = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue; // 跳过GONE的子View
}
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 一次性完成布局
child.layout(left, top, left + childWidth, top + childHeight);
// 更新下一个子View的位置
top += childHeight + getDividerHeight();
}
}
19.4 ConstraintLayout:减少嵌套的利器
说到减少嵌套,ConstraintLayout绝对是当前的最佳选择。我刚开始用的时候也不太习惯,觉得写约束太麻烦。但用了一段时间后,真香。
为什么ConstraintLayout能减少嵌套?因为它用扁平化的约束关系替代了多层嵌套的布局结构。
举个例子,以前要实现一个「顶部标题、中间内容、底部按钮」的布局,你可能需要这样写:
<!-- 传统写法:三层嵌套 -->
<LinearLayout>
<!-- 顶层 -->
<TextView />
<!-- 中间层 -->
<ScrollView>
<LinearLayout>
<!-- 内容 -->
</LinearLayout>
</ScrollView>
<!-- 底层 -->
<Button />
</LinearLayout>
用ConstraintLayout,一层就能搞定:
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ScrollView
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
性能对比:三层嵌套的LinearLayout需要3次measure传递,而ConstraintLayout只需要1次。对于复杂的界面,这个差距会成倍放大。
19.5 实战:一个优化案例
我记得有一次优化一个商品详情页。原来的布局有7层嵌套,包括RelativeLayout、LinearLayout、FrameLayout等。每次页面加载,measure时间高达45ms。
我的优化方案是这样的:
- 用一个ConstraintLayout替换掉顶层的RelativeLayout和内部的LinearLayout
- 把自定义ViewGroup的onMeasure做了缓存优化
- 移除了不必要的requestLayout()调用
优化后的结果:嵌套层数从7层降到了3层,measure时间从45ms降到了8ms。整个页面的首屏加载速度提升了40%。
注意:ConstraintLayout也不是万能的。如果你的布局只有两层嵌套,用LinearLayout反而更快。不要为了用ConstraintLayout而用,要根据实际情况选择。
19.6 知识体系总览
下面这张图总结了View测量与布局优化的核心要点:
说白了,View的测量与布局优化,核心就是做减法。减少嵌套、减少重复测量、减少不必要的布局传递。你想想看,一个界面从7层嵌套优化到3层,性能提升是立竿见影的。
我个人习惯在写布局之前,先在脑子里画一下结构图。如果发现嵌套超过3层,就会考虑用ConstraintLayout或者自定义ViewGroup来扁平化。这个习惯帮我避免了很多性能问题。
最后分享一个避坑指南:不要盲目追求「零嵌套」。有时候为了减少一层嵌套,写出来的代码可读性极差,维护成本飙升。适度优化,找到性能和可维护性的平衡点,才是正确的做法。