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耗时分析 Systrace / Layout Inspector ConstraintLayout vs LinearLayout 嵌套层级 / 测量次数 / 耗时对比 异步布局方案 Litho / AsyncLayoutInflater 自定义TraceTag 精准定位耗时 Choreographer监听 帧级别监控 嵌套层级控制 ≤3层最佳 子线程预计算 状态同步注意 核心优化原则 1. 减少嵌套层级,能用ConstraintLayout一层解决的不用多层LinearLayout 2. 异步布局适用于复杂场景,简单布局反而增加开销 3. 始终用Systrace验证优化效果,别靠感觉

布局优化没有银弹,关键是要理解每种方案的适用边界。我见过太多人盲目追求「新技术」,结果反而把性能搞得更差。记住:先测量,再优化,最后验证。用数据说话,别靠感觉。

最后说一句:如果你在优化过程中遇到「明明布局很简单,但Relayout就是慢」的情况,检查一下是不是有View频繁触发requestLayout。我遇到过某个自定义View在onDraw里调用了requestLayout,导致每帧都触发全量Relayout,这种问题用Systrace一看就现原形了。