我收集的性能优化资料和工具链
「我收集的性能优化资料和工具链」
前阵子重构一个老项目的首页,Compose 的 LazyColumn 在滚动时出现了明显的掉帧。起初我以为是 remember 和 derivedStateOf 的用法出了问题,重组次数太多,但把代码里所有的 State 梳理一遍后,帧率还是没有起色。最后在 Perfetto 里抓了一段 trace,才发现根因根本不是 Compose,而是图片解码占住了 RenderThread。这个坑花了我整整一个下午,也让我再次意识到:性能优化里最耗时的往往不是修 bug,而是定位 bug。没有合适的工具链,你只能在黑暗中猜。
Perfetto:从「感觉卡」到「看见卡在哪」
Android 10 之后,Google 彻底用 Perfetto 替换了旧的 Systrace 抓取方式。现在抓系统级 trace 的标准做法已经变成通过 adb shell perfetto 或者直接使用 Android Studio 内置的 Profiler 集成。我日常更习惯用浏览器打开 ui.perfetto.dev,把抓下来的 .perfetto-trace 文件拖进去,用 SQL 查询直接定位问题。
比如前面提到的那个掉帧问题,我在 Perfetto 的 SQL 编辑器里跑了一条查询:SELECT name, ts, dur / 1e6 AS dur_ms FROM slice WHERE dur > 16e6 AND name LIKE '%DrawFrame%',一下子就把超过 16ms 的帧给筛了出来。顺着 Choreographer#doFrame 往下滑,看到 RenderThread 里有一大段 ImageDecoder::Decode,再往上追溯到 App 的主线程,发现是 Coil 的默认 ImageRequest 没开 allowHardware(false),导致大量 bitmap 在主线程做软件解码。这种跨线程的因果关系,靠打 log 几乎不可能串起来。
Perfetto 的强项在于它能同时展示 Kernel、Native、Framework 和应用层的事件,视图完全对齐时间轴。Android 12 以后,自定义 trace 的 API 也更加成熟,Trace.beginAsyncSection() 和 Trace.endAsyncSection() 可以标记异步操作,在 Perfetto 里以完整的泳道形式展现。不过 Perfetto 的坑也很明显。trace 文件体积巨大,抓五秒钟的完整 trace 动辄几百 MB,低端机上的抓取开销甚至能改变应用本身的行为。更麻烦的是解读门槛,新手面对满屏的 binder transaction 和 irq handler 往往无从下手。我的建议是先从 App 层的自定义 trace 入手,把业务逻辑埋进去,再逐步向系统层扩展。
Android Studio Profiler 的采样陷阱
如果说 Perfetto 是看宏观的 CT,那 Android Studio Profiler 就是实验室里的显微镜。我日常用它做内存和 CPU 的精细分析。现在的 Android Studio(Iguana 乃至 Jellyfish 版本)里,Profiler 的稳定性比前几年好了不少,但坑依然不少。
CPU Profiler 提供了两种主要模式:Sample Java Methods 和 Trace Java Methods。很多刚接触性能优化的人会直接选 Trace,因为它能记录每一次函数进出,时间精确。但 Trace Java Methods 在 Dalvik 和 ART 上的开销极大,尤其是 Android 5.0 到 7.0 的 JIT 混合编译阶段,开启 Trace 后应用性能可能直接下降 50% 以上,测出来的数据完全失真。我自己更常用 Sample Java Methods,虽然它可能漏掉一些生命周期极短的方法调用,但对定位热点函数已经足够。关键是采样频率可以手动调整,默认是 1000 微秒,对 UI 线程的卡顿分析来说粒度刚好。
Memory Profiler 的 Heap Dump 是抓 Java/Kotlin 内存泄漏的利器。dump 出来后,我通常会按 Retained Size 排序,再找 Dominator Tree 里那些本不该存活的大对象。有一次我 dump 出一个 Activity 实例被某个匿名 OnClickListener 持有,导致整个页面的 View Hierarchy 都泄漏了,Retained Size 占了 12MB。但 Memory Profiler 的局限也很头疼:它对 Compose 的分配追踪不够直观,Compose 的 Slot Table 和 Recomposer 对象在 Heap Dump 里看起来就是一堆匿名的 ComposerImpl 和 MutableState,很难直接对应到业务代码。另外,dump 大型应用的内存时,Android Studio 客户端本身可能卡死,这在 Heap Size 超过 512MB 的应用上尤为常见。Native Memory 的分析直到最近几个版本才通过 Native Profiler 有所改善,但和 Java Heap 的整合还是不够好。
LeakCanary:开发期的守门员
Square 的 LeakCanary 一直是我所有 Android 项目的标配依赖,目前最新稳定版是 2.14 左右。它能在 Debug 构建里自动监测 Activity 和 Fragment 的泄漏,一旦检测到就会在通知栏弹出一个鲨鱼图标,点开就是完整的引用链。这个工具节省了我无数时间,尤其是项目里用了大量 RxJava 或者自定义 Listener 的时候。
LeakCanary 2.x 用 Kotlin 彻底重写后,分析速度比早期的 1.x 快了很多,Heap Dump 的解析也放到了独立的进程中,减少了对主线程的影响。但它绝非万能。最明显的局限是只能监控 Java/Kotlin Heap,对 Native 层的泄漏(比如 Bitmap 的 native allocation、JNI 里的 malloc)完全无能为力。另外,每次泄漏发生时它都要触发一次 Heap Dump,在低端机上这个操作会让应用冻结 3 到 5 秒,体验极差。还有一些系统级别的已知泄漏,比如旧版本 Android 的 InputMethodManager 和 TextLine,LeakCanary 虽然内置了忽略规则,但偶尔还是会因为厂商定制 ROM 的差异而误报。我通常会在 leak_canary_watcher_watch_duration 里把观察时间从默认的 5 秒延长到 10 秒,减少一些因为页面跳转动画没结束导致的假阳性。
LeakCanary 是开源免费的,GitHub 上直接搜 square/leakcanary 就能找到。个人项目直接拉最新版就行,企业项目里如果接了多进程架构,记得把 AppWatcher.config 里的 watchersToInstall 按进程过滤一下,避免重复监控。
Macrobenchmark 与 Baseline Profile:实验室里的冷启动
Jetpack Macrobenchmark 库(目前 1.2.x 版本)是我做启动优化时的主要工具。它的核心思路是在一个受控的测试环境里,反复执行某个用户旅程(比如冷启动到首页),然后统计帧时间、启动耗时等指标。写 Macrobenchmark 时有一个绕不开的坑:你必须在开发者选项里把「窗口动画缩放」「过渡动画缩放」「动画时长缩放」全部关闭,否则测出来的帧时间会把系统动画算进去,毫无意义。
Macrobenchmark 的另一个重要用途是生成 Baseline Profile。通过 BaselineProfileRule,你可以让测试机器人跑一遍核心路径,然后自动生成一个 baseline-prof.txt 文件。这个文件在打包进 APK 后,能被 Android 7.0(API 24)以上的 ART 在 AOT 编译时参考,把关键路径的代码直接编译成机器码,跳过解释执行。Android Gradle Plugin 8.1 以后,Google 提供了 baselineProfile DSL,配合 BaselineProfileGenerator 可以自动把 profile 合并进 assets。国内不上 Google Play 的应用需要手动把这个文件放进 src/main/assets/ 或者通过 build.gradle 的 sourceSets 指定路径,这多了一步维护成本。
Baseline Profile 的效果是真实的,但期望要合理。它主要优化的是冷启动,因为热启动时代码大概率已经被 JIT 编译过了。另外,它对 Pixel 设备的依赖很强,不同厂商的 ROM 对 AOT 编译的调度策略差异很大,有些国产 ROM 甚至会为了省电忽略掉第三方应用的 profile。Macrobenchmark 本身的运行时间也是