第三讲:内存监控与优化——从指标到实战
大家好,欢迎来到第三讲。
内存优化,说白了就是跟系统“抢资源”。你想想看,Android 系统给每个 App 分配的内存是有上限的,一旦超了,轻则卡顿,重则直接闪退。我这些年做性能优化,踩过最多的坑就在内存上。今天咱们就把内存这块彻底聊透。
一、内存类型:PSS、USS、RSS 到底看哪个?
很多同学刚接触内存优化时,会被 PSS、USS、RSS 这几个缩写搞晕。我刚开始也一样,对着 dumpsys 的输出一脸懵。其实它们很好理解,咱们一个一个说。
| 指标 | 全称 | 含义 | 典型场景 |
|---|---|---|---|
| RSS | Resident Set Size | 进程实际占用的物理内存(包含共享库) | 看进程“吃”了多少物理内存 |
| PSS | Proportional Set Size | 按比例分摊共享库后的内存 | 系统级内存统计,最常用 |
| USS | Unique Set Size | 进程独占的物理内存(不包含共享) | 判断内存泄漏,最准确 |
RSS 是“实打实”占用的内存,但它有个问题:如果两个进程共享同一个库,RSS 会把这份内存算两次。所以用 RSS 看单个进程还行,看系统整体就不准了。
PSS 就聪明多了。它把共享库的内存按进程数均摊。比如一个 3MB 的共享库被 3 个进程使用,每个进程只算 1MB。我个人习惯用 PSS 做系统级监控,因为它最接近“真实消耗”。
USS 是进程独有的内存,不包含任何共享部分。这个指标在检测内存泄漏时特别有用。为什么?因为泄漏的内存通常是进程自己 new 出来的对象,不会被共享。USS 持续上涨,基本可以断定有泄漏。
核心结论:
- 看系统整体内存压力 → 用 PSS
- 判断单个进程是否泄漏 → 用 USS
- 看进程物理占用 → 用 RSS(但别太当真)
二、dumpsys meminfo:最常用的内存诊断工具
这个命令我几乎每天都会敲。用法很简单:
adb shell dumpsys meminfo <包名>
输出内容很多,咱们挑重点看。
2.1 关键字段解读
输出中有一个 “TOTAL” 行,它下面会列出 PSS、Private Dirty、Private Clean 等。我重点看两个:
- Java Heap:Java 层堆内存,GC 主要管这块。
- Native Heap:C/C++ 层堆内存,很多泄漏藏在这里。
有一次我排查一个图片加载 OOM 的问题,Java Heap 很正常,但 Native Heap 飙到了 200MB。后来发现是 Skia 底层没释放 Bitmap 的像素数据。嗯,这里要注意,Native 内存泄漏比 Java 更难定位。
2.2 实战技巧:连续采样
单次 dumpsys 只能看快照,要判断内存是否在增长,需要连续采样:
for i in $(seq 1 10); do
adb shell dumpsys meminfo com.example.app | grep "TOTAL"
sleep 2
done
如果 TOTAL PSS 持续上涨且不回落,那基本可以断定有泄漏。我曾经用这个脚本抓到一个第三方 SDK 的内存泄漏,对方还不承认,我把数据甩过去,他们才乖乖修了。
小技巧: 用 dumpsys meminfo --local 可以只看当前进程,省去包名。适合在 adb shell 里直接跑。
三、/proc/meminfo:系统级内存全景图
这个文件是 Linux 内核暴露的内存信息接口。Android 底层也是 Linux,所以它同样适用。
adb shell cat /proc/meminfo
输出内容很多,我挑几个关键字段:
| 字段 | 含义 | 关注点 |
|---|---|---|
| MemTotal | 总物理内存 | 设备硬件规格 |
| MemFree | 完全空闲的内存 | 太低说明内存紧张 |
| MemAvailable | 可用内存(含可回收的缓存) | 比 MemFree 更实用 |
| Buffers / Cached | 缓存和缓冲区 | 系统会动态调整 |
| SwapTotal / SwapFree | 交换分区(部分设备有 zRAM) | zRAM 过高可能影响性能 |
MemAvailable 是我最常看的。它考虑了可回收的缓存,比 MemFree 更能反映“实际可用内存”。如果 MemAvailable 低于总内存的 10%,系统就开始杀后台进程了。你想想看,这时候你的 App 很可能被系统“请走”。
注意: /proc/meminfo 是全局信息,不能用来定位具体进程的问题。想看进程级内存,还得用 dumpsys meminfo 或者 /proc/<pid>/smaps。
四、内存泄漏检测:LeakCanary 原理
说到内存泄漏检测,LeakCanary 是绕不开的工具。我最早用 1.x 版本,那时候还需要手动初始化。现在 2.x 版本已经做到“零配置”了,加个依赖就行。
4.1 核心原理:WeakReference + ReferenceQueue
LeakCanary 的原理其实不复杂。它利用 Java 的 WeakReference 和 ReferenceQueue 机制:
- 当一个对象只被 WeakReference 引用时,GC 可以随时回收它。
- 对象被回收后,WeakReference 会被放入 ReferenceQueue。
- 如果一段时间后,ReferenceQueue 里没有这个 WeakReference,说明对象没有被回收 → 泄漏了。
LeakCanary 就是盯着 Activity 和 Fragment 的 onDestroy() 方法。一旦调用,它就创建一个 WeakReference 指向该 Activity,然后等 5 秒(默认)。5 秒后如果 WeakReference 还在,就触发一次 GC,再等一会。如果还在,那就 dump 堆快照,分析引用链。
4.2 源码级别的关键步骤
我简化一下 LeakCanary 的核心流程:
// 伪代码,展示核心逻辑
class LeakCanary {
fun watch(activity: Activity) {
val ref = KeyedWeakReference(activity, referenceQueue)
// 延迟 5 秒检查
handler.postDelayed({
if (!ref.isEnqueued()) {
// 对象没有被回收,触发 GC
Runtime.getRuntime().gc()
// 再等一会
handler.postDelayed({
if (!ref.isEnqueued()) {
// 确认泄漏,dump heap
dumpHeapAndAnalyze(ref)
}
}, 1000)
}
}, 5000)
}
}
这里有个关键点:KeyedWeakReference。LeakCanary 没有直接用 WeakReference,而是自己封装了一个,加了一个 key 字段。这样在分析堆快照时,可以通过 key 快速找到对应的引用。
4.3 实战中的坑
我曾经在一个项目里集成 LeakCanary,结果发现它总是报某个 Fragment 泄漏。排查了半天,发现是 Fragment 的 onDestroy() 没有被调用——因为我们在 Activity 的 onSaveInstanceState() 里移除了 Fragment,但没调用 remove 方法。嗯,这其实不是 LeakCanary 的锅,是我们代码的问题。
避坑指南:
- LeakCanary 只在 debug 版本使用,release 版本一定要排除。
- 如果遇到“假泄漏”,检查一下是不是 Lifecycle 回调没走对。
- 堆快照文件可能很大(几十 MB),注意手机存储空间。
五、总结:内存优化的三板斧
好了,这一讲的内容不少。我帮你捋一下核心思路:
- 看指标:PSS 看整体,USS 看泄漏,RSS 做参考。
- 用工具:dumpsys meminfo 查进程,/proc/meminfo 看系统。
- 抓泄漏:LeakCanary 自动检测,原理是 WeakReference + ReferenceQueue。
下一讲咱们聊聊 CPU 监控与优化。到时候我会分享一个我亲手写的 CPU 采样工具,保证让你大开眼界。
公众号:蓝海资料掘金营,微信deep3321