11、布局与测量优化:Relayout耗时分析、ConstraintLayout vs LinearLayout性能对比、异步布局方案
布局优化,说白了就是跟「测量」和「布局」这两个阶段死磕。我做了这么多年性能优化,发现大部分卡顿问题,根源都在布局嵌套太深、测量次数太多。今天咱们就聊聊怎么精准定位Relayout的耗时,以及几种主流布局方案的真实性能差异。
11.1 Relayout耗时分析——找到罪魁祸首
先问个问题:你知道一次Relayout到底干了多少活吗?
Android的布局流程,从ViewRootImpl.performTraversals()开始,会依次触发measure、layout、draw。但真正耗时的,往往是measure和layout这两个阶段。我习惯用Systrace来抓取Relayout的完整调用链,配合Layout Inspector看视图层级,基本能定位到问题。
核心思路:Relayout的触发条件包括——子View请求重新布局、View尺寸变化、可见性变化、动画更新等。每次Relayout都会从根View开始递归遍历整棵树。
举个例子,我在项目中遇到过这样一个场景:一个列表项里嵌套了5层LinearLayout,每次滑动都触发全量Relayout。用Systrace一看,单次measure耗时超过12ms,直接导致掉帧。后来我把嵌套层级压到3层以内,耗时降到了3ms以下。
如何精准测量Relayout耗时?
我推荐两种方式:
- Systrace + 自定义TraceTag:在关键View的onMeasure和onLayout里手动添加Trace.beginSection(),配合systrace.py抓取,能看到每个View的耗时分布。
- Choreographer + FrameCallback:通过Choreographer监听每一帧的绘制时间,如果某帧的布局时间超过16ms,基本就是Relayout拖了后腿。
// 自定义TraceTag示例
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Trace.beginSection("MyView.onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Trace.endSection();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Trace.beginSection("MyView.onLayout");
super.onLayout(changed, left, top, right, bottom);
Trace.endSection();
}
我的小技巧:如果Relayout耗时集中在某个父容器上,试试用ViewStub延迟加载不立即显示的视图,或者用merge标签减少嵌套层级。我曾经把一个5层嵌套的布局改成2层,帧率直接从45fps提升到58fps。
11.2 ConstraintLayout vs LinearLayout——性能真相
网上有很多文章说ConstraintLayout比LinearLayout快,但实际情况没那么简单。我做过一组对比测试,结果如下:
| 布局类型 | 嵌套层级 | 测量次数 | 平均耗时(ms) | 适用场景 |
|---|---|---|---|---|
| LinearLayout | 3层 | 1次 | 2.1 | 简单线性排列 |
| LinearLayout | 6层 | 3次 | 8.7 | 复杂嵌套 |
| ConstraintLayout | 1层 | 2次 | 3.5 | 复杂相对布局 |
| ConstraintLayout | 2层 | 2次 | 4.2 | 带约束链 |
看到了吗?LinearLayout在简单场景下其实更快,因为它测量逻辑简单。但一旦嵌套层级超过3层,LinearLayout的递归测量次数会指数级增长,这时候ConstraintLayout的优势就体现出来了——它用一层布局就能搞定复杂的相对关系。
我个人习惯是:能用一层ConstraintLayout解决的,绝不用多层LinearLayout。但如果是简单的水平或垂直排列,LinearLayout反而更轻量。
注意:ConstraintLayout的约束链(Chain)如果使用不当,会导致两次测量。我见过有人在一个ConstraintLayout里塞了20多个View,全部用约束链连接,结果测量耗时比6层LinearLayout还高。别滥用约束链,能用相对定位就用相对定位。
11.3 异步布局方案——让UI不再阻塞主线程
嗯,这里要聊点进阶的东西。传统的布局都在主线程执行,一旦布局复杂,主线程就被卡住了。异步布局的思路,就是把measure和layout放到子线程去做,主线程只负责渲染。
目前主流的异步布局方案有几种:
- Litho:Facebook开源的异步布局框架,用Yoga引擎做Flexbox布局,支持增量渲染。我在一个信息流项目里用过,首屏加载速度提升了40%。
- AsyncLayoutInflater:Android官方提供的异步inflate方案,但只支持异步加载布局文件,不支持异步measure/layout。
- 自定义异步布局:通过HandlerThread或协程,在子线程完成measure和layout,然后把结果post回主线程。
我重点说说Litho。它的核心思想是:布局计算是纯函数,不依赖主线程状态。所以可以在子线程预计算布局结果,生成一个LayoutResult,主线程直接拿来渲染就行。
// Litho异步布局示例
@LayoutSpec
class MyComponentSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c) {
return Row.create(c)
.paddingDip(16)
.child(Text.create(c)
.text("Hello Litho")
.textSizeDip(14))
.build();
}
}
// 在子线程异步计算布局
ComponentContext context = new ComponentContext(activity);
Component component = MyComponent.create(context).build();
LayoutState.calculate(context, component, widthSpec, heightSpec);
关键点:异步布局不是银弹。如果你的布局本身很简单(比如只有几个TextView),异步布局的开销反而比直接在主线程布局更大。我建议只在以下场景使用:列表项布局复杂、首屏需要快速展示、动画过程中需要频繁Relayout。
我曾经在一个电商App的商品详情页里,用Litho替换了原来的嵌套LinearLayout。那个页面有商品图、标题、价格、规格选择、评价等十几个模块,原来主线程布局耗时约35ms。换成Litho后,布局计算放到子线程,主线程只负责渲染,帧率从30fps稳定到了55fps。
避坑指南:异步布局最大的坑是「状态同步」。子线程计算布局时,如果依赖了主线程的某个状态(比如用户是否登录),必须保证状态的一致性。我遇到过子线程读取了过期的状态,导致布局渲染错误。解决方案是用不可变数据模型,或者加版本号校验。
11.4 知识体系总览
下面这张图总结了本章的核心逻辑,从问题定位到方案选型,再到异步布局的落地路径:
布局优化没有银弹,关键是要理解每种方案的适用边界。我见过太多人盲目追求「新技术」,结果反而把性能搞得更差。记住:先测量,再优化,最后验证。用数据说话,别靠感觉。
最后说一句:如果你在优化过程中遇到「明明布局很简单,但Relayout就是慢」的情况,检查一下是不是有View频繁触发requestLayout。我遇到过某个自定义View在onDraw里调用了requestLayout,导致每帧都触发全量Relayout,这种问题用Systrace一看就现原形了。