我收集的性能优化资料和工具链

我收集的性能优化资料和工具链

我收集的性能优化资料和工具链


「我收集的性能优化资料和工具链」


前阵子重构一个老项目的首页,Compose 的 LazyColumn 在滚动时出现了明显的掉帧。起初我以为是 rememberderivedStateOf 的用法出了问题,重组次数太多,但把代码里所有的 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 transactionirq handler 往往无从下手。我的建议是先从 App 层的自定义 trace 入手,把业务逻辑埋进去,再逐步向系统层扩展。


Android Studio Profiler 的采样陷阱


如果说 Perfetto 是看宏观的 CT,那 Android Studio Profiler 就是实验室里的显微镜。我日常用它做内存和 CPU 的精细分析。现在的 Android Studio(Iguana 乃至 Jellyfish 版本)里,Profiler 的稳定性比前几年好了不少,但坑依然不少。


CPU Profiler 提供了两种主要模式:Sample Java MethodsTrace 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 里看起来就是一堆匿名的 ComposerImplMutableState,很难直接对应到业务代码。另外,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 的 InputMethodManagerTextLine,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.gradlesourceSets 指定路径,这多了一步维护成本。


Baseline Profile 的效果是真实的,但期望要合理。它主要优化的是冷启动,因为热启动时代码大概率已经被 JIT 编译过了。另外,它对 Pixel 设备的依赖很强,不同厂商的 ROM 对 AOT 编译的调度策略差异很大,有些国产 ROM 甚至会为了省电忽略掉第三方应用的 profile。Macrobenchmark 本身的运行时间也是

TelephonyManager 的监听限制,Android 10 后的变化 2026-08-24
MockK 和 Mockito 的对比,Kotlin 项目怎么选 2026-08-26

评论区