第2章:收集启动阶段耗时数据

启动速度优化的第一步是“可量化”。只有精确测量每个阶段的耗时,才能找到瓶颈并验证优化效果。本章将介绍如何使用 systrace(现已被 Perfetto 取代)抓取 Android 应用启动 Trace,解析启动流程中的关键阶段,通过标记自定义锚点(Trace.beginSection / Trace.endSection)来细化测量粒度,并最终输出结构化的“耗时树”用于对比分析。

2.1 测量工具选型:Systrace vs Perfetto

特性 Systrace (原) Perfetto (推荐)
底层机制 基于 atrace / ftrace Perfetto 跟踪服务 + TraceProcessor
抓取方式 python systrace.py 脚本 perfetto SDK / cmdline / UI
数据格式 HTML 或 CTF protobuf (.perfetto-trace)
自定义锚点 Trace.beginSection/endSection 同左(兼容)+ Perfetto SDK 命名追踪
解析能力 较简单,缺乏 SQL 查询 TraceProcessor 支持 SQL/脚本分析
适用版本 Android 10 之前 Android 10+ / 所有版本
推荐策略:从 Android 12 起官方不再更新 systrace 工具包,建议全量迁移至 Perfetto。即使旧项目仍可用 systrace,也建议将解析脚本迁移到 TraceProcessor,以获得 SQL 查询和自定义耗时树生成能力。

2.2 抓取启动 Trace 的命令与配置

2.2.1 使用 Perfetto 命令行抓取

安装 Perfetto 命令行工具(推荐通过 prebuilts 或 SDK manager 下载),使用以下命令抓取冷启动 Trace:

# 1. 停止一切已有跟踪(释放 buffer)
adb shell perfetto --reset

# 2. 启动跟踪,设置 buffer 36MB,准备捕获 10 秒数据
adb shell perfetto \
  -t 10s \
  -b 36mb \
  -o /data/misc/perfetto-traces/app_start.trace \
  --txt-config - <<EOF
buffers: {
  size_kb: 36864
}
data_sources: {
  config {
    name: "android.track_event"
  }
}
data_sources: {
  config {
    name: "android.surfaceflinger.frametimeline"
  }
}
data_sources: {
  config {
    name: "android.app_operations"
  }
}
duration_ms: 10000
EOF

# 3. 在跟踪启动后立即冷启动应用(在另一终端执行)
adb shell am start -W -S com.your.app/.MainActivity

# 4. 等待 10s 跟踪结束,拉取文件
adb pull /data/misc/perfetto-traces/app_start.trace
关键参数说明:
-t 10s:跟踪持续秒数,冷启动通常 10s 足够;
-b 36mb:环形 buffer 大小,过小会导致数据丢失;
--txt-config -:通过 stdin 传递配置,更灵活;
android.track_event:系统应用自定义锚点的基础数据源;
am start -W -S:先杀死进程再启动(冷启动场景),并等待 Activity 显示。

2.2.2 使用 Perfetto UI 可视化分析

将拉取的 .perfetto-trace 文件上传到 https://ui.perfetto.dev,即可看到可视化的时间轴。UI 支持搜索、切片、分组和导出 CSV。

2.3 解析启动流程:四大关键阶段

一次标准的冷启动 Trace 中,主要出现以下阶段(按时间顺序):

  1. Application.onCreate —— 应用进程启动后的第一个回调用,常包含大量第三方 SDK 初始化。
  2. Activity.onCreate —— 界面创建,布局 inflate、ViewModel 初始化等。
  3. Activity.onStart —— 界面可见但不可交互,通常轻量。
  4. Activity.onResume —— 首次真正可交互(用户可见 & 可触摸)。
  5. FrameTimeline —— 系统 surfaceflinger 提供的首帧、全帧渲染时间线。
观察技巧:在 Perfetto UI 中,打开左侧面板的“Scrolling”选项,然后按“M”键标记出“Application”、“Activity”等系统切片。如果发现 Application.onCreate 耗时 > 2s,则极大概率是第三方 SDK 阻塞了主线程。

2.4 标记自定义锚点:精细化分割

系统提供的自带切片粒度太粗,无法精准定位某个初始化函数的具体耗时。我们需要在代码中插入自定义 Trace 锚点。

2.4.1 Android Java/Kotlin 代码锚点

import android.os.Trace;

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        // 开始自定义跟踪——必须在 super.onCreate() 之后,否则 Trace 未初始化
        super.onCreate();
        Trace.beginSection("MyApp:onCreate");

        // 模拟 SDK 初始化
        Trace.beginSection("SDK_A_Init");
        initSDKA();
        Trace.endSection();

        Trace.beginSection("SDK_B_Init");
        initSDKB();
        Trace.endSection();

        Trace.beginSection("DB_Open");
        openDatabase();
        Trace.endSection();

        Trace.endSection(); // MyApp:onCreate
    }

    private void initSDKA() {
        // ... 实际初始化代码
    }
}
常见错误:不要忘记 super.onCreate() 之后再创建 Trace 段,否则系统可能尚未注册 Trace 机制导致 beginSection 无效。同时务必成对调用 beginSection/endSection,嵌套时必须严格先进后出。不匹配会导致 Trace 数据混乱甚至崩溃。

2.4.2 NDK/C++ 层锚点(若使用原生代码)

在 CMakeList.txt 中添加 Perfetto 依赖(或直接使用 atrace API):

# 使用 atrace(system/core/libcutils/ include/cutils/trace.h)
#include <cutils/trace.h>

void nativeInit() {
    ATRACE_BEGIN("native:InitSDK_C");
    // ... native 逻辑
    ATRACE_END();
}

若项目已集成 Perfetto SDK,推荐使用 TRACE_EVENT_BEGIN/END 宏以获得更好的跨进程追踪能力。

2.5 生成耗时树:从 Trace 数据到可视化报表

拿到原始 Trace 后,我们需要解析出分阶段的耗时,并按调用栈层次输出“耗时树”。Perfetto 内置的 TraceProcessor 可以通过 Python 或 SQL 解析结构化数据。

2.5.1 使用 TraceProcessor Python API

from perfetto.trace_processor import TraceProcessor

tp = TraceProcessor(trace="app_start.perfetto-trace")

# 查询所有自定义切片(包括系统切片)
query = """
SELECT
  name,
  dur AS duration_ns,
  depth
FROM slice
WHERE name GLOB '*MyApp*' OR name GLOB '*SDK*'
ORDER BY ts ASC
"""
df = tp.query(query).as_pandas_dataframe()

# 输出耗时树(按 depth 打印缩进)
def print_tree(group_iter, depth=0):
   prefix = "  " * depth
   for _, row in group_iter:
       dur_ms = row['duration_ns'] / 1_000_000
       print(f"{prefix}{row['name']}: {dur_ms:.2f} ms")

# 简单打印(更复杂的父子关系需用 sqlite 递归 CTE)
print_tree(df.iterrows())

输出示例:

MyApp:onCreate: 1234.56 ms
  SDK_A_Init: 450.23 ms
  SDK_B_Init: 320.89 ms
  DB_Open: 150.12 ms
Activity:onCreate: 210.45 ms
Activity:onResume: 45.67 ms
Total cold start (launch to first frame): 1890.12 ms

2.5.2 耗时树可视化(SVG 图)

下图为一次冷启动 Trace 解析后,按阶段划分的耗时树(示例数据):

启动阶段耗时树(示例数据 - 单位ms) Total Cold Start 1890ms MyApp:onCreate 1234ms Activity:onCreate 210ms Activity:onResume 46ms SDK_A_Init 450ms SDK_B_Init 320ms DB_Open 150ms 耗时树解读 • 根节点 Total Cold Start 包含进程创建 + 所有应用层初始化。 • 第一层子节点显示各主要回调耗时,易于发现“最胖”阶段。 • 第二层子节点为 Application.onCreate 内的自定义锚点,精准定位 SDK 瓶颈。 • 本例中 SDK_A_Init (450ms) + SDK_B_Init (320ms) 占 Application 总时长的 62%, 是最优先优化的对象(可通过懒加载、异步或延迟初始化来缩减)。 💡 提示:实际 trace 中还要考虑系统本身耗时(Zygote fork、ClassLoader 加载等),本章聚焦应用侧。

2.6 自动输出耗时树脚本

为方便日常集成,建议将解析逻辑封装为一个 Python 脚本 gen_start_trace_tree.py,放到 CI 或一键测试工具中:

#!/usr/bin/env python3
"""gen_start_trace_tree.py - 从 Perfetto trace 生成启动耗时树(JSON/Text)"""
import sys, json
from perfetto.trace_processor import TraceProcessor

def main(trace_path):
    tp = TraceProcessor(trace=trace_path)
    # 查询所有切片 name 包含 'MyApp' 或 'Activity' 的自定义切片
    df = tp.query("""
        SELECT name, dur, depth, parent_id
        FROM slice
        WHERE name LIKE '%MyApp%' OR name LIKE '%Activity%' OR name LIKE '%SDK%'
        ORDER BY ts
    """).as_pandas_dataframe()

    # 构建树结构(简化:用 depth 做缩进)
    tree_lines = []
    for _, row in df.iterrows():
        indent = "  " * row['depth']
        dur_ms = row['dur'] / 1_000_000
        tree_lines.append(f"{indent}{row['name']}: {dur_ms:.2f} ms")

    # 输出文本树
    output = "\n".join(tree_lines)
    print(output)

    # 可选输出 JSON 供前端展示
    with open("startup_tree.json", "w") as f:
        json.dump({"nodes": df.to_dict(orient="records")}, f, indent=2)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("usage: gen_start_trace_tree.py <trace.perfetto-trace>")
        sys.exit(1)
    main(sys.argv[1])

2.7 常见错误与排查锦囊

  • Trace 文件为空或非常小:检查 data_sources 配置中是否遗漏 android.track_event,或者 buffer 太小导致覆盖。
  • 自定义锚点不显示:确认代码中 Trace.beginSection 与 Trace.endSection 配对;检查 minSdk 是否需要 android:debuggable="true"(部分设备要求);部分系统级 trace 默认不抓取用户自定义切片,需在配置中显式开启 ftrace_events: "atrace/trace_markers"。
  • 耗时树深度不对或漏掉子节点:使用 Perfetto UI 的“查询”面板运行 SELECT name, depth, dur FROM slice WHERE depth < 10,确认数据本身是否完整。
  • 时间戳不对齐导致总耗时与组件耗时不一致:注意 am start -W 返回的 ThisTime 可能包含系统活动启动耗时的额外部分,建议以 Trace 中 StartupActivity 系统切片为准。
部署建议:在 CI 流程中,每次应用包构建后自动执行一次冷启动抓取 + 耗时树生成,对比 Diff,当某个阶段耗时增加超过阈值(如 200ms)时标记为回归。持续监控才能保持启动速度稳定。

2.8 本章小结

通过本章,你已经掌握了:

  • Perfetto 命令行抓取启动 Trace 的完整配置;
  • 在 Java / C++ 代码中插入自定义锚点的方法;
  • 使用 TraceProcessor Python API 解析耗时并生成可读的耗时树;
  • 常见问题排查手段。

接下来的章节将基于此测量结果,实战优化 Application.onCreate 与 Activity 生命周期的瓶颈,逐步削减启动耗时。