第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+ / 所有版本 |
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 中,主要出现以下阶段(按时间顺序):
- Application.onCreate —— 应用进程启动后的第一个回调用,常包含大量第三方 SDK 初始化。
- Activity.onCreate —— 界面创建,布局 inflate、ViewModel 初始化等。
- Activity.onStart —— 界面可见但不可交互,通常轻量。
- Activity.onResume —— 首次真正可交互(用户可见 & 可触摸)。
- FrameTimeline —— 系统 surfaceflinger 提供的首帧、全帧渲染时间线。
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 解析后,按阶段划分的耗时树(示例数据):
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系统切片为准。
2.8 本章小结
通过本章,你已经掌握了:
- Perfetto 命令行抓取启动 Trace 的完整配置;
- 在 Java / C++ 代码中插入自定义锚点的方法;
- 使用 TraceProcessor Python API 解析耗时并生成可读的耗时树;
- 常见问题排查手段。
接下来的章节将基于此测量结果,实战优化 Application.onCreate 与 Activity 生命周期的瓶颈,逐步削减启动耗时。