1. 内存泄漏概述:什么是内存泄漏、为什么重要

说实话,干了这么多年Android性能优化,我最怕听到的就是线上用户反馈“App用着用着突然闪退”或者“点个按钮等半天没反应”。排查到最后,十有八九跟内存泄漏有关。内存泄漏这东西,说白了就是本该被回收的对象,却因为某些引用一直活着,像下水道里卡住的头发丝——越积越多,最后彻底堵死。

1.1 到底什么是内存泄漏?

官方定义很拗口:“不再需要的对象被垃圾回收器(GC)判定为可达而无法回收”。我换个说法:你用完一把螺丝刀,随手放回工具箱。但如果有人一直握着它不放,你想用也用不了,工具箱里还多占一个位置。程序里的对象也是这个道理——当某个Activity退出后,它的实例本该被GC清理,但如果有静态变量或匿名内部类持有它的引用,它就永远“赖着不走”。

核心特征: 对象不可用(业务上已废弃)但不可达(GC无法标记为垃圾)。

举个例子,我几年前优化一个社交App的“个人主页”时发现,用户反复进入退出该页面,内存占用就只升不降。用MAT一分析——原来Fragment里有个匿名Handler,发送了一个延迟消息,而这个Handler隐式持有外部Activity的引用。Activity销毁后,消息队列里还挂着这个Handler,Activity死活没法回收。你看看,这就是典型的“隐式泄漏”。

1.2 内存泄漏的常见“元凶”

我个人在实践中总结过一张表,基本涵盖了90%的Android泄漏场景:

泄漏类型 根本原因 典型实例 解决方案(一句话)
静态变量泄漏 static字段持有Activity/Context 单例保存Activity引用 改用Application Context或弱引用
非静态内部类泄漏 内部类隐式持有外部类引用 匿名Runnable、Handler 使用静态内部类+弱引用
资源未关闭 Cursor、Stream、IO忘记close 数据库查询后未关闭Cursor try-with-resources或在finally中关闭
注册未注销 BroadcastReceiver、EventBus、观察者未unregister Activity中注册了动态广播 在onDestroy中反注册
集合类泄漏 对象放入集合后不再移除 HashMap中的Key长期存活 使用WeakHashMap或及时remove

你可能会问:“这些看起来都是基础知识点,写过Android的都知道啊?” 嗯,道理都懂,但在复杂业务里很容易被忽视。我曾经在优化一个视频编辑App时,发现一个工具类里有个static的ArrayList用来缓存历史操作,结果随着用户不断撤销重做,这个List无限膨胀,最后直接OOM。你看,理论落到实践,就是另一回事了。

1.3 为什么内存泄漏“必须重视”?

很多人觉得“内存泄漏嘛,大不了GC多回收几次”。可实际上,泄漏的代价远不止堆空间被占。我梳理了三个主要影响:

  • 直接OOM: 堆内存被泄漏对象吃光,系统直接杀掉进程。用户感知就是闪退。
  • 触发ANR: 堆空间紧张时,GC会频繁触发Full GC,每次停顿几十到几百毫秒,这些停顿可能就卡在UI线程上。用户点了个按钮,等了5秒没反应,系统弹出“应用无响应”。
  • 性能雪崩: 内存不足触发系统LMK(Low Memory Killer)杀后台进程,杀完又重启,来回折腾,手机发热、耗电快。
曾经踩过的坑: 某个视频播放器的播放页,用户从首页点击进入播放页再返回,这样重复几十次后必现ANR。我用TraceView一看,UI线程上有大量GC时间片。原因就是播放页的MediaPlayer(被Activity持有)一直没释放,加上系统为了维持播放状态又持有其他资源,长链泄漏导致内存飙升。那次排查花了我整整两天……

1.4 内存泄漏的“蝴蝶效应”

我把从泄漏发生到最后造成卡死/ANR的完整链条画了一张图,你看一眼应该就全明白了:

内存泄漏 → ANR/OOM 影响链 泄漏发生过程 后果 对象存活(业务已结束) 意外引用(泄漏根源) GC无法回收 堆内存持续增长 OOM (闪退) ANR (应用无响应) 频繁GC → UI卡顿 注意:一次泄漏可能同时导致多个后果 实际ANR常伴随GC停顿 + 锁竞争

你看这个链条:业务结束后对象本该死,但被意外引用拖住 → GC觉得它“还活着” → 堆内存只增不减 → 系统为了腾空间频繁GC(卡顿) → 堆吃满后直接OOM,或者长时间GC冻结UI线程导致ANR。我当年那个视频播放器的ANR,就是第4步到第6步这条路径。

1.5 快速自检:你写的代码有没有泄漏风险?

分享一个我平时做Code Review时检查的checklist:

  • 静态变量或单例里有没有Activity/Context/View引用?
  • 匿名内部类(尤其是Runnable、Handler)是否可能在对象销毁后仍在执行?
  • 广播、观察者、callback有没有成对注册/注销?
  • Bitmap、File、Cursor用完有没有关闭?
  • 集合类是否无限增长而没有清理机制?
一个小技巧: 在Activity的onDestroy中故意调用System.gc(),然后用Memory Profiler抓一次heap dump,如果发现该Activity实例还存在,基本就是泄漏了。我以前靠这招揪出了好几个隐蔽的泄漏。

1.6 本章要点整理

好,咱们把这一章的核心记下来:

  • 内存泄漏 = 无用对象被GC误认为有用,本质是引用链没有断开。
  • 泄漏直接导致堆内存膨胀,进而引发OOM、ANR、卡顿这三种最让用户抓狂的问题。
  • 最常见的场景:静态变量、内部类、资源未关闭、注册未注销、集合类。
  • 排查手段:借助Memory Profiler、MAT、LeakCanary,结合人工Code Review。