第2章 泄漏现象识别与初步判断

2.1 现象识别:ANR / OOM / 卡顿 / 内存抖动

Android 应用中由 Lifecycle 引发的内存泄漏,最终会在用户侧或系统侧表现为四大类可观测现象。准确识别这些现象,是排查链条的第一环。下表列出了每类现象的核心特征、典型触发条件以及初始怀疑方向:

现象类别核心特征典型触发场景初始怀疑方向
ANR(应用无响应)系统弹出「应用无响应」对话框;Logcat 中出现 ANR in ... 标签;主线程阻塞超过 5sActivity 或 Service 销毁后,持有其引用的后台线程继续执行耗时操作(如网络请求、数据库写入)长生命周期对象(Fragment、AsyncTask、Thread、Listener)未在 onDestroy 中释放
OOM(内存溢出)应用崩溃,Logcat 打印 OutOfMemoryError;堆内存持续增长直至触发 kill大量 Activity 实例泄漏导致 Bitmap、View 树等无法回收;频繁创建大尺寸对象且不被 GCActivity / 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 数据采集:日志、堆转储与性能跟踪

在观察到异常现象后,应立即开始采集以下三类数据。数据采集的时机一旦错过,泄漏现场往往难以复现。

  1. 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
  1. 堆转储(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 时的堆快照。
  1. 运行时性能跟踪:使用 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% 时自动触发一次堆转储。
内存泄漏现象识别与初步定位决策树 现象观察 ANR / 长时间阻塞 OOM / 溢出崩溃 卡顿 / 内存抖动 Logcat ANR traces HPROF 堆转储 Systrace / Allocation 主线程堆栈 是否在 onDestroy 卡住 Activity 实例计数 GC Root 路径分析 Allocation 曲线 GC 后堆是否回落 修复 → 复测 → 预防 决策规则: ① 主线程堆栈停留 in onDestroy → 怀疑匿名内部类 / Handler 泄漏; ② Activity 实例 > 3 个且 GC 后不减少 → 静态引用或长生命周期持有;③ Allocation 锯齿+堆不回缩 → 临时对象+泄漏并存。

图:内存泄漏现象识别与初步定位决策树。从外部可见现象出发,通过三类数据采集,最终收敛到泄漏类型判断,为后续修复提供方向。