第29章 性能优化工具链进阶:Perfetto深度使用、SimplePerf CPU采样、R8编译优化、Baseline Profiles

性能优化这件事,说白了就是「找到瓶颈,然后干掉它」。但怎么找?靠猜?靠感觉?我见过太多开发者,上来就怀疑某个函数慢,结果折腾半天,发现瓶颈根本不在这。

这一章,我带你走一遍真正的工具链。从系统级追踪到CPU采样,从编译优化到启动加速,每一步都有实战价值。

29.1 Perfetto:系统级追踪的瑞士军刀

Perfetto 是 Google 推出的新一代性能追踪工具。它取代了老的 systrace,功能强了不止一个量级。

我个人习惯,遇到卡顿问题,第一件事就是拉一份 Perfetto 记录。为什么?因为它能看到整个系统的行为——不仅仅是你的 App,还有系统服务、GPU、CPU 调度、内存分配……

29.1.1 基本使用

抓取 Perfetto 记录有两种方式:

  1. 命令行方式:适合自动化脚本
  2. Perfetto UI:适合交互式分析

先看命令行:

# 抓取 10 秒的追踪数据
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace \
  -t 10s \
  sched freq idle am wm gfx view binder_driver hal dalvik

# 拉取到本地
adb pull /data/misc/perfetto-traces/trace.perfetto-trace .

然后在浏览器打开 ui.perfetto.dev,把文件拖进去。嗯,界面很直观。

我的经验:抓取时间不要太长,10-15 秒足够。太长的话文件会很大,分析起来也费劲。我一般只抓取复现问题的那几秒。

29.1.2 关键分析技巧

打开 Perfetto 界面后,你会看到一堆彩色条。别慌,我教你几个关键点:

  • 看 CPU 调度:你的线程是不是频繁被调度出去?如果是,说明 CPU 竞争激烈。
  • 看 Binder 调用:跨进程调用是不是耗时太长?我曾经发现一个 App 卡顿,就是因为频繁调用系统服务获取传感器数据。
  • 看 SurfaceFlinger:掉帧问题,多半和它有关。看看合成时间是不是超过了 16ms。

举个例子,你看到某个线程长时间处于 Runnable 状态但没被调度,那说明 CPU 不够用了。这时候优化方向就是减少计算量,或者把任务移到后台线程。

29.1.3 自定义 Trace 点

Perfetto 最强大的地方,是你可以自己埋点。在代码里加上:

// 在关键路径上埋点
Trace.beginSection("loadUserData");
// ... 你的业务逻辑 ...
Trace.endSection();

然后在 Perfetto 里就能看到这段代码的执行时间。我习惯在启动流程、列表滑动、网络请求这几个关键路径上都埋上点。这样一旦出问题,直接看 trace 就能定位。

注意:埋点不要太多,否则 trace 文件会膨胀。我一般只埋耗时超过 10ms 的操作。细粒度的函数调用,用 SimplePerf 更合适。

29.2 SimplePerf:CPU 采样分析

Perfetto 擅长看系统级行为,但如果你想知道「哪个函数占用了最多的 CPU 时间」,那就得用 SimplePerf。

SimplePerf 是 Android 自带的 CPU 性能分析工具。它支持两种模式:

  • 采样模式:每隔一段时间记录当前执行的代码位置
  • 计数模式:统计 CPU 事件(如缓存未命中)的次数

我主要用采样模式。它开销小,适合线上问题分析。

29.2.1 基本用法

# 对 App 进程进行 CPU 采样,持续 10 秒
adb shell simpleperf record -p $(pidof com.example.app) \
  -e cpu-cycles:u -f 4000 --duration 10 -o /data/local/tmp/perf.data

# 生成报告
adb shell simpleperf report -i /data/local/tmp/perf.data \
  --sort comm,dso,symbol --percent 1

输出结果会告诉你,哪些函数占用了最多的 CPU 周期。我遇到过最典型的情况:一个看似简单的 JSON 解析,居然占了 30% 的 CPU 时间。后来换成手动解析,性能直接翻倍。

29.2.2 火焰图

SimplePerf 还能生成火焰图。火焰图这东西,说白了就是「看哪个函数调用栈最宽」。越宽的函数,占用的 CPU 时间越多。

# 生成火焰图数据
adb shell simpleperf report -i /data/local/tmp/perf.data \
  --full-callgraph -o report.txt

# 用脚本转换成火焰图
python flamegraph.py --input report.txt --output flame.svg

打开生成的 SVG 文件,你会看到一堆堆叠的矩形。最顶层的矩形就是热点函数。我习惯从最宽的那个开始看,往往能发现意想不到的瓶颈。

避坑指南:采样频率不要设太高。4000 Hz 已经够用了。太高的话,采样本身会消耗 CPU,反而影响结果。我曾经设过 10000 Hz,结果采样进程占用了 20% 的 CPU,数据完全失真。

29.3 R8 编译优化:让代码更小更快

R8 是 ProGuard 的替代品。它不仅能混淆代码,还能做深度优化。我见过很多团队只开了混淆,没开 R8 的全量优化,白白浪费了性能提升的机会。

29.3.1 开启 R8 全量优化

gradle.properties 里加上:

android.enableR8.fullMode=true

这个开关会启用 R8 的所有优化策略,包括:

  • 内联:把短函数直接嵌入调用处,减少调用开销
  • 常量传播:把编译期就能确定的常量直接替换
  • 死代码消除:删除永远不会执行的代码
  • 类合并:把多个类合并成一个,减少类加载开销

我做过一个实验:开启 fullMode 后,APK 体积减少了 15%,启动速度提升了 8%。效果很明显。

29.3.2 自定义规则

R8 的规则和 ProGuard 基本一致。但有几个点要注意:

# 保留反射调用的类
-keep class com.example.reflection.** { *; }

# 保留序列化相关
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
}

# 保留 JNI 方法
-keepclasseswithmembernames class * {
    native <methods>;
}

我踩过一个坑:某个第三方库用了反射,我没加 keep 规则,结果线上崩溃了。从那以后,我养成了一个习惯——每次升级第三方库,都先跑一遍 R8 的 --printusage 选项,看看哪些类被删除了。

警告:R8 的优化很激进。如果你用了动态加载、反射、或者 Java 的 ServiceLoader,一定要加 keep 规则。否则,编译时好好的,运行时直接崩溃。

29.4 Baseline Profiles:启动加速的终极武器

Baseline Profiles 是 Android 12 引入的新特性。它的原理很简单:把 App 启动时需要用到的类和代码,提前编译成机器码。这样启动时就不用边解释边执行了。

29.4.1 生成 Baseline Profiles

Google 官方推荐用 Jetpack Macrobenchmark 来生成。但我个人觉得,手动配置更可控。

首先,在 src/main 目录下创建 baseline-prof.txt

# 启动路径
HSPLcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/example/app/MainActivity;->onResume()V
HSPLcom/example/app/HomeFragment;->onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View;

# 常用类
HSPLcom/example/app/data/UserRepository;->getUser()Lcom/example/app/model/User;
HSPLcom/example/app/network/ApiService;->fetchData()Lretrofit2/Call;

# 第三方库
HSPLokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
HSPLcom/google/gson/Gson;->fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;

然后,在 build.gradle 里配置:

android {
    ...
    baselineProfile {
        // 指定 profile 文件路径
        from("src/main/baseline-prof.txt")
    }
}

编译后,APK 里会包含 assets/dexopt/baseline.prof 文件。系统在安装 App 时,会根据这个文件提前编译。

29.4.2 效果验证

我测试过一个项目:没有 Baseline Profiles 时,冷启动需要 1.2 秒。加上之后,降到了 0.8 秒。提升了 33%。

但要注意,Baseline Profiles 只对冷启动有效。热启动和温启动,因为代码已经在内存里了,效果不明显。

我的建议:Baseline Profiles 不是写一次就完事了。每次版本迭代,都要重新生成。因为代码路径变了,旧的 profile 可能已经失效。我一般会在每个 Release 分支上,用自动化脚本重新跑一遍生成流程。

29.5 知识体系总览

这一章的内容比较多,我画了一张图帮你理清思路:

性能优化工具链进阶 Perfetto 系统级追踪 • CPU调度分析 • Binder调用追踪 • SurfaceFlinger分析 • 自定义Trace点 • 命令行/UI两种方式 SimplePerf CPU采样分析 • 采样/计数模式 • 热点函数定位 • 火焰图生成 • 调用栈分析 • 低开销线上可用 R8编译优化 代码缩减与加速 • 内联优化 • 常量传播 • 死代码消除 • 类合并 • 自定义Keep规则 Baseline Profiles 启动加速 • 预编译热点代码 • 冷启动加速30%+ • 手动/自动生成 • 版本迭代更新 • 配合Macrobenchmark 从系统追踪 → CPU采样 → 编译优化 → 启动加速,形成完整优化闭环

这四块工具,各有各的用途。Perfetto 看宏观,SimplePerf 看微观,R8 做静态优化,Baseline Profiles 做动态加速。结合起来用,效果最好。

我个人的工作流是这样的:先用 Perfetto 定位卡顿发生的阶段,然后用 SimplePerf 找到具体的热点函数,接着用 R8 优化代码,最后用 Baseline Profiles 加速启动。一套下来,App 的性能通常能有明显提升。

嗯,工具链就讲到这里。下一章,我会讲怎么把这些工具集成到 CI/CD 流水线里,实现自动化性能监控。到时候,你就不用每次都手动抓 trace 了。


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