Perfetto 内存分析:用 heapprofd 抓住 Android 内存泄漏
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
凌晨的告警群弹出一条消息:某购物应用线上 OOM 率在过去两小时抬升,OutOfMemoryError的堆栈里夹着一段 2MB 的Bitmap分配失败。监控平台上那条 RSS 曲线更刺眼——从用户打开应用那一刻起,进程驻留内存像台阶一样逐级上爬,40 分钟后被lmkd一把杀掉。奇怪的是,LeakCanary 在开发机上跑同样的操作,什么也没抓出来。这类"复现不出来、总量看着还行、却越用越肿"的 Android 内存优化难题,往往不是靠多打几个 log 能解决的。真正需要的是一个能把每一块未释放内存钉到具体调用栈上的工具——Perfetto 里的 heapprofd 模块,正是干这个的。
先把内存长在哪想清楚
拿到一条异常曲线,最忌讳的动作是"一把梭"地开全量 trace。先问一句:涨的是 Native Heap 还是 Dalvik Heap?用adb shell dumpsys meminfo <pid>看两行数字就能定调。如果 Native Heap 在涨,即便你的 App 一行 C/C++ 都没写也正常——java.util.regex这类框架 API 底层就是 native 分配。此时选 heapprofd:它 hook 住malloc/free,把"分配了没释放"的字节归到调用栈上,源码在 src/profiling/。
如果涨的是 Dalvik Heap,情况分两种。想看"哪些对象在活着、谁牵着谁不放",用 ART heap dump(要求 Android 11+),它给你一张活对象可达性图,能回溯整个堆;但代价是看不到对象"是在哪一行 new 出来的"。想反过来盯"对象创建点的分配压力",用 ART 分配剖析(--heaps com.android.art,要求 Android 12+),它按调用栈聚合创建点,适合查 Java 侧的 churn。
图1:Perfetto heapprofd 火焰图,顶部是线程入口,越往下越接近真正调用 malloc 的代码
还有一类容易被漏掉:直接mmap的大块内存。它不走 malloc,heapprofd 天然看不见。这类量通常不大、出现频率低,可以用linux.perf设period: 1逐次采样,或走 ftrace 的 mmap 系统调用事件(Android 14+)单独抓。别指望一个 trace 解决所有问题,先把内存"分类"再动手,能少走大量弯路。
采样参数:一张表定生死
heapprofd 是采样式的——每分配n字节,平均命中一次采样并补记n字节,靠这个比例把开销压下来。默认n = 4096。几个参数值得你下笔前就选好,而不是事后重抓:
| 参数 | 默认 | 作用 | 何时改大 / 改小 |
|---|---|---|---|
-i/--interval | 4096 | 采样间隔(字节) | 分配速率极高、缓冲溢出时调大到 16000+;查小对象泄漏时调小 |
--shmem-size | 8MiB | 客户端到 heapprofd 的环形缓冲 | 瞬时分配尖峰导致提前结束时调大 |
-c | 0(关) | 连续快照间隔(ms) | 想画"随时间爬升"曲线时设 5000 |
-d | 0(到 Ctrl-C) | 采集时长(ms) | 复现路径固定时设死,便于前后对比 |
--dump-at-max | 关 | 按峰值而非结束时导出 | 峰值转瞬即逝、结尾已回落时启用 |
图2:Perfetto 录制页勾选 Native heap profiling 后填写进程名,可直接从浏览器抓
这里有个反直觉的坑,我当年栽过一次。
⚠️误区:只看总内存只盯 RSS 或
dumpsys的总占用是危险的。总量平稳不代表没泄漏——某个对象类型持续累积,只要还没触发 GC 回收,总量曲线照样"健康"。盯"未释放内存的占比"和"对象生命周期"这两个维度,比盯总量靠谱得多。
⚠️误区:指望它追溯过去heapprofd不是回溯式的,只记录 trace 开始之后的分配。问"现在为什么这么大"它答不了,问"接下来还会不会继续涨"它最在行。要抓启动阶段,就得在进程还没起来时先开
-n <进程名>,让它跟着 zygote 一起从出生跟到死亡。
三个案例,三种抓法
参数只是入场券,真正决定成败的是"针对现象选对姿势"。下面三个案例各自独立,读完你会发现套路会自己长出来。
案例一:夜间模式切换后的 15MB 不释放
某社交 App 切一次深色模式,Native Heap 加 15MB 且再也不回。我按复现路径开-d 15000,切完立刻收,火焰图里AssetManager一路高下去。关键动作是在过滤器里敲关键字,让火焰图聚焦到那条栈,一眼看出ThemeManager用 Activity 上下文缓存了整套样式。把上下文换成ApplicationContext,重跑同一条命令对比,增量从 15MB 掉到 2MB。前后各抓一次、同一命令、同一时长,对比才有说服力——别让"感觉变好了"替代数据。
案例二:新闻列表越滚越涨
列表类问题要看"随时间的形状",单张快照不够。这里开连续快照-c 5000,UI 里会铺出一排 slice,每个 slice 是一段窗口的未释放汇总,鼠标拖选连续几个还能合并统计。滚 30 条再停,曲线是标准台阶——每次上滑叠一级,几乎不释放。切到 "Unreleased Malloc Count" 而非 Size,小对象泄漏立刻显形:单张几 KB,但 Bitmap 缓存没回收,数量堆起来就是灾难。修复回收逻辑后,同样 30 次操作,曲线平了。
图3:ART 分配剖析视图,按调用栈聚合 Java 对象创建点,适合查 churn
案例三:启动阶段的隐性堆积
前两个都能现场复现,启动泄漏最磨人——用户冷启动那一下的分配,等你连上 adb 早没了。解法是用进程名而非 PID:-n com.example.app会命中之后新拉起的所有同名进程,从 zygote 特化那一刻就开始采。收完拿 SQL 把调用栈一次性拉平,比在火焰图里一层层翻快得多:
INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree; SELECT name, mapping_name, cumulative_size FROM android_heap_profile_summary_tree ORDER BY abs(cumulative_size) DESC;这条查询按"该函数出现在栈上任意位置"累计未释放字节,降序排完,头部几行就是启动期没放回去的常驻块,对着mapping_name直接翻代码。
进阶与长期机制
把偶发问题变成持续防线,靠的是把工具接进流程。三条能立刻做的:
- 自定义分配器也要能被抓。你手写的内存池走的是自己的
my_malloc,heapprofd 看不见。用 Custom Allocator API(Android 10+)把每次分配/释放上报进去即可,伪代码:
auto heap = AHeapProfile_registerHeap(AHeapInfo_create("image_cache")); // my_malloc 内: AHeapProfile_reportAllocation(heap, ptr, size); // my_free 内: AHeapProfile_reportFree(heap, ptr);离线符号化与混淆还原。栈里全是地址或混淆名时,对 trace 跑
trace_processor做符号化,Java 侧再喂 ProGuard map 还原;否则你抓到的只是"第 47 帧 unknown"。版本差异心里有数。heapprofd 最低 Android 10,32 位程序在 64 位设备上不能采;ART 分配剖析要 12+;Android 12 上 Java 帧回溯偶发缺名,13 QPR1 才修稳。低版本设备上的"空 profile"多半不是泄漏,是目标进程根本不 eligible——
user构建下只有debuggable或profileable的 App 能采。
把这些落成制度:CI 里固定抓启动 + 一次核心用户旅程两段 trace,设内存预算做回归告警,每个季度全量审计一次。Perfetto 的 trace_processor 支持 SQL 批量解析,意味着这套流程能脱离人眼、跑成脚本——docs/ 里有完整的分析文档,tools/heap_profile 是采集入口。
可立即落地的四件事:
- 用
dumpsys meminfo先分清 Native / Dalvik,再决定用 heapprofd 还是 ART heap dump。 - 复现路径固定的,用
-d定死时长、-c 5000开连续快照,前后各抓一次做 diff。 - 启动泄漏用
-n <进程名>冷启动采集,收完用summary_tree模块一次拉平调用栈。 - 把上面这套写进 CI,配一条内存预算回归线。
工具摆在你手边了,命令也就这么几行。真正的问题是——你上一次看那条越爬越高的 RSS 曲线时,是"顺手刷过去了",还是"停下来抓了一帧"?
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考