第三讲:内存监控与优化——从指标到实战

大家好,欢迎来到第三讲。

内存优化,说白了就是跟系统“抢资源”。你想想看,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 机制:

  1. 当一个对象只被 WeakReference 引用时,GC 可以随时回收它。
  2. 对象被回收后,WeakReference 会被放入 ReferenceQueue。
  3. 如果一段时间后,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),注意手机存储空间。

五、总结:内存优化的三板斧

好了,这一讲的内容不少。我帮你捋一下核心思路:

  1. 看指标:PSS 看整体,USS 看泄漏,RSS 做参考。
  2. 用工具:dumpsys meminfo 查进程,/proc/meminfo 看系统。
  3. 抓泄漏:LeakCanary 自动检测,原理是 WeakReference + ReferenceQueue。

下一讲咱们聊聊 CPU 监控与优化。到时候我会分享一个我亲手写的 CPU 采样工具,保证让你大开眼界。

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