第2章 泄漏现象识别与初步判断
2.1 现象识别:ANR / OOM / 卡顿 / 内存抖动
Android 应用中由 Lifecycle 引发的内存泄漏,最终会在用户侧或系统侧表现为四大类可观测现象。准确识别这些现象,是排查链条的第一环。下表列出了每类现象的核心特征、典型触发条件以及初始怀疑方向:
| 现象类别 | 核心特征 | 典型触发场景 | 初始怀疑方向 |
|---|---|---|---|
| ANR(应用无响应) | 系统弹出「应用无响应」对话框;Logcat 中出现 ANR in ... 标签;主线程阻塞超过 5s | Activity 或 Service 销毁后,持有其引用的后台线程继续执行耗时操作(如网络请求、数据库写入) | 长生命周期对象(Fragment、AsyncTask、Thread、Listener)未在 onDestroy 中释放 |
| OOM(内存溢出) | 应用崩溃,Logcat 打印 OutOfMemoryError;堆内存持续增长直至触发 kill | 大量 Activity 实例泄漏导致 Bitmap、View 树等无法回收;频繁创建大尺寸对象且不被 GC | Activity / Fragment 实例泄漏 + 大对象滞留(如 View、Drawable、Adapter) |
| 卡顿(Jank) | 界面滑动、转场掉帧;Systrace / FrameMetrics 显示帧渲染时间超过 16ms;掉帧频率随操作时间递增 | 泄漏的 Context 引用导致 View 树更新开销膨胀;单例持有大量 Activity 实例造成遍历开销 | 匿名内部类、注册的 Listener、未注销的 BroadcastReceiver 导致主线程额外负担 |
| 内存抖动(Allocation Thrashing) | GC 频繁触发(Allocation Rate > 10 MB/s);Logcat 出现 GC_CONCURRENT、GC_FOR_ALLOC;堆内存利用率低但吞吐量高 | 短生命周期对象(如临时 String、Handler 发送的消息对象)在循环中大量创建,且被泄漏对象间接持有 | View 动画、ListView / RecyclerView 中频繁创建临时对象且未复用;泄漏的 Holder 持有临时资 |
现场判断口诀:看日志找
ANR / OutOfMemoryError / jank 关键字;看 Heap 走势图是否阶梯式上升;看 Allocation Tracker 是否有连续尖峰。如果三者均符合,基本可判定存在泄漏性内存问题。2.2 数据采集:日志、堆转储与性能跟踪
在观察到异常现象后,应立即开始采集以下三类数据。数据采集的时机一旦错过,泄漏现场往往难以复现。
- Logcat 日志:优先捕获
am_anr、ActivityManager: ANR、OutOfMemoryError、CONNECTION等标签。使用命令:
adb logcat -v threadtime -b main -b events -b system -b crash | grep -E "ANR|OOM|OutOfMemory|am_anr|ActivityManager: ANR" > anr_oom.log
- 堆转储(HPROF):在卡顿或 OOM 发生前/后手动 dump。推荐使用
adb shell am dumpheap或 Android Studio Profiler 的 Dump Java Heap 按钮:
adb shell am dumpheap com.example.app /sdcard/dump.hprof
adb pull /sdcard/dump.hprof ./
若应用已 ANR 或 OOM 崩溃,可以尝试开启 “总是 dump heap on ANR”(开发者选项)来自动保存 ANR 时的堆快照。
- 运行时性能跟踪:使用 Systrace / Perfetto 记录 10~30 秒包含卡顿过程的 trace。重点关注
java.lang.Thread.sleep、android.os.Binder、android.view.ViewRootImpl等耗时函数。命令示例:
adb shell perfetto -c /data/misc/perfetto-configs/track_event.cfg -o /data/misc/perfetto-traces/trace.perfetto-trace
# 或使用 Android Studio 的 CPU Profiler 直接录制
采集避坑:堆转储过程本身会触发一次 GC,可能临时“释放”泄漏对象,导致 dump 结果不准确。建议在泄漏发生后的 2~3 次 GC 之后立即 dump,或在 Profiler 中设置「Dump before next GC」。
2.3 初步定位:三看定位法
拿到日志、hprof 和 trace 后,按以下“三看”优先级进行快速定位:
- 一看 ANR 堆栈:从
traces.txt或 Logcat 的 ANR 信息中找到主线程堆栈。若堆栈长时间停留在Activity.onStop()、Fragment.onDestroy()等回调内,且被非主线程持有锁,则极有可能是 Lifecycle 回调中的同步泄漏。 - 二看 HPROF 中的泄漏链:使用 MAT 或 LeakCanary 打开 hprof。搜索
Activity或Fragment实例数量。若某个 Activity 实例数量超过 5 个且均无法被回收,展开 GC Root 路径,检查是否被以下常见泄漏源持有:- 匿名内部类(
XXX$1,XXX$2) - 静态引用(static 字段持有 Activity)
- 未注销的 Listener / Observer(如
LocationManager,SensorManager,LiveData) - Handler 或 Thread 持有的 MessageQueue 消息
- 匿名内部类(
- 三看内存抖动曲线:在 Profiler 的 Memory 面板中查看 Allocation Rate(分配速率)与 GC 频率。若 allocation 曲线呈高频锯齿状,且每次 GC 后堆大小不回落至基线,说明存在泄漏性抖动。
2.4 修复动作:按泄漏类型分级应对
初步定位后,可立即执行低成本的修复动作来止损:
- 若为静态引用泄漏:将静态字段置为
null或改为WeakReference。 - 若为匿名内部类泄漏:将内部类改为静态内部类(取消对外部类的隐式引用),或在
onDestroy中显式取消任务。 - 若为未注销的 Listener:在
onDestroy中调用对应的unregister*()方法。 - 若为 Handler 泄漏:移除所有回调并销毁线程池,或使用
Lifecycle.Event.ON_DESTROY触发清理。
记录每次修复的变更和对应的现象指标(如 ANR 次数、堆增长斜率),以便后续复测。
2.5 复测:现象是否消除
执行修复后,使用自动化脚本模拟泄漏场景(如快速旋转屏幕、跳转页面后返回、打开长列表后退出),并再次采集日志和 hprof:
# 旋转屏幕 20 次
for i in {1..20}; do
adb shell am broadcast -a android.intent.action.CONFIGURATION_CHANGED
sleep 1
done
# 抓取 heap 并统计 Activity 数量
adb shell am dumpheap com.example.app /sdcard/dump_after.hprof
read -p "分析 dump_after.hprof,确认 Activity 数量是否回落至 1 或 2"
若 ANR/OOM 不再重现,且堆增长率从阶梯下降转为平稳,则复测通过。
2.6 预防:建立监控与编码规范
- 上线前:集成 LeakCanary 并在 CI/CD 管道中运行
adleak检测。 - 代码规约:所有 Fragment / Activity 中注册的监听器、回调、Handler、Thread 必须在
onDestroy中释放;禁止在静态上下文保存 View 或 Drawable 引用。 - 运行时监控:使用 Android Vitals(Play Console)或自建 APM 监测 ANR 率和 OOM 率;当卡顿帧比例 >5% 时自动触发一次堆转储。
图:内存泄漏现象识别与初步定位决策树。从外部可见现象出发,通过三类数据采集,最终收敛到泄漏类型判断,为后续修复提供方向。