16、启动时间测量方法:bootchart工具使用,ftrace与systrace在启动分析中的应用,自定义启动时间打点

启动优化这件事,说白了就是「先得知道时间花在哪了」。你连瓶颈在哪都不知道,怎么优化?

我在项目里见过不少同学,一上来就闷头改代码,改完发现启动时间反而更长了。嗯,这就是典型的「没有测量就没有优化」。今天我们就来聊聊,Android 启动时间到底怎么测,用什么工具测,以及如何自己埋点。

16.1 启动时间测量的整体思路

先看一张图,帮你快速建立知识框架:

启动时间测量方法全景图 bootchart 进程级启动时序 ftrace 内核级函数追踪 systrace 用户态 + 内核态 特点: • 无需修改代码 • 看进程启动顺序 特点: • 内核函数级追踪 • 精度高,开销小 特点: • 图形化界面 • 适合分析卡顿 自定义启动时间打点 Trace.beginSection() / Trace.endSection() 核心原则:先测量,后优化。不同阶段用不同工具。

从图上你能看到,三个工具各有侧重。bootchart 适合看「谁先谁后」,ftrace 适合深挖内核,systrace 适合看整体流程。而自定义打点,则是你手里的「万能补丁」。

16.2 bootchart:最省事的启动分析工具

bootchart 是我个人最喜欢的工具。为什么?因为它不需要你改任何代码,开箱即用。

16.2.1 如何开启 bootchart

在 Android 10 之后,bootchart 默认是关闭的。你需要手动触发:

# 在 adb shell 中执行
touch /data/bootchart/enabled
reboot

重启后,系统会在 /data/bootchart/ 目录下生成数据文件。用 adb pull 拉下来,然后用 bootchart 工具解析:

# 拉取数据
adb pull /data/bootchart/ ./bootchart_data

# 解析生成 SVG 图
bootchart -f svg ./bootchart_data
小提示: 如果你没有 bootchart 命令,可以用 Python 脚本替代。AOSP 源码里自带了一个 system/core/init/bootchart.cpp,编译出来就能用。

16.2.2 怎么看 bootchart 图

生成的 SVG 图里,横轴是时间,纵轴是进程。你会看到:

  • CPU 使用率曲线:顶部那条,如果一直跑不满,说明瓶颈在 IO 或锁。
  • 进程启动时间线:每个进程从创建到就绪的时间。
  • 磁盘读写:底部区域,看有没有大量读操作拖慢启动。

我曾经在一个项目里,发现 system_server 启动后等了 2 秒才开始启动 Launcher。一看 bootchart 图,发现有个第三方服务在 init.rc 里写了 class late_start,结果它占着 CPU 不放。嗯,改完就快了 1.5 秒。

注意: bootchart 只能看到进程级别的信息。如果你想看某个函数调了多少次、花了多久,它做不到。这时候就得请出 ftrace 了。

16.3 ftrace:内核级别的「显微镜」

ftrace 是 Linux 内核自带的追踪工具。说白了,它能告诉你内核里每个函数被调用的时间。

16.3.1 基本用法

ftrace 通过 /sys/kernel/tracing/ 下的文件来控制。我常用的步骤:

# 进入 trace 目录
cd /sys/kernel/tracing

# 选择追踪器
echo function_graph > current_tracer

# 设置要追踪的函数(比如你想看 init 进程)
echo do_execve > set_ftrace_filter

# 开始追踪
echo 1 > tracing_on

# 跑你的测试(比如重启)
# ...

# 停止追踪
echo 0 > tracing_on

# 查看结果
cat trace

输出会是一长串函数调用树,像这样:

 1)               |  do_execve() {
 1)   0.123 us    |    getname();
 1)   0.456 us    |    count();
 1)   0.789 us    |    prepare_bprm_creds();
 1)               |    exec_binprm() {
 1)   1.234 us    |      load_elf_binary();
 1)   0.567 us    |      flush_old_exec();
 1)   2.345 us    |    }
 1)   5.678 us    |  }

你看,每个函数的耗时一目了然。我在调优内核驱动时,就靠这个找到了一个 SPI 驱动初始化时多等了 200ms 的原因——它读了一个不存在的寄存器。

16.3.2 启动阶段用 ftrace 抓什么

我个人建议重点关注这几个点:

  • init 进程的 do_execve:看 init 启动花了多久。
  • 文件系统挂载:比如 mount、do_mount 相关函数。
  • 驱动 probe:比如 platform_drv_probe,看哪个驱动慢。
核心技巧:set_ftrace_pid 只追踪特定进程,避免被其他进程的输出淹没。比如只追踪 init 进程:echo 1 > set_ftrace_pid(1 是 init 的 PID)。

16.4 systrace:用户态 + 内核态的统一视图

systrace 是 Google 官方推荐的性能分析工具。它把用户态的 Trace 和内核态的 ftrace 数据合并在一起,生成一个 HTML 文件,用 Chrome 打开就能看。

16.4.1 抓取启动阶段的 systrace

启动阶段的 systrace 需要在开机过程中抓取。我常用的命令:

# 在 adb shell 中执行
echo 1 > /sys/kernel/tracing/tracing_on

# 然后重启设备(保持 adb 连接)
adb reboot

# 等设备启动后,拉取 trace
adb shell cat /sys/kernel/tracing/trace > boot_trace.txt

# 或者用 atrace 直接抓
adb shell atrace --async_start -b 4096 am wm view
# 重启后
adb shell atrace --async_dump > boot_trace.html
小技巧: 如果你用的是 Android 12 以上,可以直接用 adb shell perfetto 来抓,功能更强大。perfetto 是 systrace 的下一代工具。

16.4.2 怎么看 systrace 图

打开生成的 HTML 文件,你会看到:

  • CPU 调度:每个 CPU 核心在跑什么进程。
  • 进程状态:Running、Runnable、Sleeping 等。
  • 自定义 Trace 段:如果你在代码里打了点,这里会显示。

我曾经遇到一个 case:Launcher 启动后白屏了 3 秒。用 systrace 一看,发现 Launcher 的主线程在等一个 Binder 调用,而那个 Binder 服务在 system_server 里,system_server 又在等一个 IO 操作。这一串依赖链,在 systrace 里看得清清楚楚。

16.5 自定义启动时间打点

工具再好,也有覆盖不到的地方。比如你想精确测量「从开机到 Launcher 绘制第一帧」的时间,bootchart 和 systrace 都做不到。这时候就需要自己埋点。

16.5.1 在 Java 层打点

Android 提供了 Trace 类,用法很简单:

import android.os.Trace;

public class MyApp {
    public void onCreate() {
        Trace.beginSection("MyApp.onCreate");
        // 你的初始化代码
        Trace.endSection();
    }
}

这样在 systrace 里就能看到 MyApp.onCreate 这个段了。

16.5.2 在 Native 层打点

Native 代码用 ATrace

#include <cutils/trace.h>

void my_init() {
    ATRACE_BEGIN("my_init");
    // 你的初始化代码
    ATRACE_END();
}

16.5.3 在 init.rc 里打点

这个可能很多人不知道。init.rc 里也能打点:

on boot
    # 开始一个 trace 段
    write /sys/kernel/tracing/trace_marker "C|1|boot_start|0"
    # 你的启动命令
    start core
    # 结束 trace 段
    write /sys/kernel/tracing/trace_marker "C|1|boot_start|1"
注意: 直接写 trace_marker 需要 root 权限。在 user 版本上可能被 SELinux 拦截。我建议用 ATRACE 宏,它会自动处理权限问题。

16.5.4 实战:测量「开机到 Launcher 第一帧」

我在一个项目中,需要精确测量这个时间。做法是:

  1. 在 Launcher 的 onFirstFrame 回调里打点。
  2. 在 init 进程启动时打一个起始点。
  3. 用 systrace 抓取,然后看两个点之间的时间差。

代码大概这样:

// 在 Launcher 中
public class Launcher extends Activity {
    @Override
    public void onFirstFrame() {
        Trace.beginSection("Launcher.firstFrame");
        Trace.endSection();
    }
}

然后在 systrace 里搜索 Launcher.firstFrame,看它离 init 的起始点有多远。这个时间就是「真正的启动时间」。

经验之谈: 自定义打点不要打太多。我见过有人一个 Activity 里打了 50 个点,结果 systrace 图密密麻麻,根本看不清。建议只打关键路径上的点,比如:Application.onCreate、Activity.onCreate、onFirstFrame。

16.6 总结与避坑

好了,我们来快速回顾一下:

  • bootchart:适合快速看进程启动顺序,不需要改代码。
  • ftrace:适合深挖内核函数,精度高。
  • systrace:适合看整体流程,结合用户态和内核态。
  • 自定义打点:适合精确测量特定阶段的时间。

我曾经踩过一个坑:用 bootchart 分析时,发现某个服务启动很慢,以为是它的问题。结果用 ftrace 一查,发现是内核的 IO 调度器在搞鬼——它把读请求排在了写请求后面。嗯,这就是「工具选对了,问题就解决了一半」。

最后送你一句话:没有测量就没有优化,但测量本身也要选对工具。


公众号:蓝海资料掘金营,微信deep3321