Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

内存优化与泄漏排查

内存问题直接影响 App 的存活率(OOM)和流畅度(频繁 GC 导致卡顿)。对于底层背景开发者,native 内存泄漏排查会是很好的加分项。

一、常见的内存泄漏场景

内存泄漏(Memory Leak)的本质是:长生命周期的对象(如单例、静态变量、系统级线程)持有了短生命周期对象(如 Activity/Fragment)的强引用,导致短生命周期对象无法被 GC 回收

Java / Kotlin 泄漏重灾区

  1. 静态变量 / 单例:静态变量持有 Activity 的 Context 或 View 实例。
  2. 非静态内部类 / 匿名类:如 Handler 延时消息未移除、Runnable 在子线程还在跑,它们隐式持有外部类的引用。
  3. 未注销监听器:广播接收器 BroadcastReceiver、EventBus 等未在 onDestroy 注销。
  4. 协程 / Flow:未跟随组件生命周期(如在 Activity onDestroy 后协程仍在运行),或全局 ViewModel 持有了 UI 元素。
  5. 资源未关闭CursorStreamFileDescriptor 没有 close,或者 Bitmap 未及时 recycle

二、内存抖动与 GC 机制

  • 内存抖动 (Memory Churn):短时间内频繁创建大量临时小对象(如在 onDraw 或大循环内 new 对象),导致可用内存迅速耗尽,触发频繁 GC。
  • 影响:Dalvik/ART 的 GC 过程可能会产生应用停顿(Stop-The-World,尽管 ART 有并发 GC,但仍有停顿和 CPU 开销),从而引发界面卡顿甚至掉帧。
  • 优化:使用对象池复用对象,避免在高频调用的路径(如 UI 渲染)中进行堆分配。

三、Bitmap 与 Native 内存

现代 Android (8.0+) 的 Bitmap 像素数据存放于 Native 内存中,这大大缓解了 Java 堆 OOM 的压力,但也带来了 Native 泄漏的风险。

  • Native OOM:当系统总可用物理内存不足或虚拟地址空间耗尽时,Native 层同样会发生分配失败(OOM)。
  • 排查手段:Native 内存的追踪较难,可以通过 malloc_debugHWASan/ASan 等底层工具进行内存越界和泄漏排查。

四、排查路径与流程(本地 vs 线上)

诊断策略需区分开发期和生产环境:

诊断层级工具与手段适用场景
本地定位LeakCanary、Memory Profiler、MAT开发期主动发现泄漏、详细分析引用链
线上监控内存阈值报警、KOOM(快手开源的线上 OOM 治理)、APM 平台捕获真实用户场景 OOM、轻量级 dump 和裁剪上报

泄漏排查流程:

  1. 现象发现:观察到应用内存占用持续走高或收到 OOM 奔溃报告。
  2. 触发 Dump:在开发阶段,利用 Memory Profiler 点击强制 GC 并抓取 Java Heap (hprof)。LeakCanary 会在后台对象保留超时后自动 dump。
  3. 寻找 GC Roots:将 hprof 导入 Memory Profiler 或 MAT (Memory Analyzer Tool)。寻找怀疑泄漏的对象实例(如 MainActivity)。
  4. 分析引用链 (Reference Chain):查看从 GC Root 到该泄漏实例的最短强引用路径
  5. 切断引用:修改代码,将该路径上的长生命周期引用置为 null、改用弱引用 (WeakReference),或在合适的生命周期节点手动解绑。

五、LeakCanary 原理剖析

LeakCanary 是如何自动发现泄漏的?

  1. Hook 生命周期:自动注册 Application.ActivityLifecycleCallbacks(或利用 Fragment/ViewModel 钩子)监听对象销毁。
  2. 弱引用观察:对象销毁后,用它构建一个带有 ReferenceQueue 的弱引用,交由 ObjectWatcher 观察,并标记个超时时间(如 5s)。
  3. 判断回收:超时后强制触发一次 GC,如果该弱引用没有出现在 ReferenceQueue 中,说明对象还被强引用,标记为 retained
  4. 抓取分析:若 retained 对象数达阈值,dump 整个 hprof 文件,交由内嵌的 Shark 库解析 hprof,找出最短强引用链并发送通知。

高频面试题

Q1: 内部类一定会导致内存泄漏吗? 不一定。只有非静态内部类才会隐式持有外部类的引用。且只有当内部类的生命周期长于外部类时(比如被 static 变量引用,或跑在一个没结束的子线程里),才会造成外部类无法释放,形成泄漏。静态内部类不持有外部类引用。

Q2: Handler 导致的内存泄漏怎么解决? 将 Handler 声明为静态内部类,内部通过 WeakReference<Activity> 持有 Activity 以便更新 UI。更关键的是,在 Activity onDestroy 中调用 handler.removeCallbacksAndMessages(null) 清除所有未执行的消息和任务。

Q3: Android 8.0 之后 Bitmap 内存存在哪里?如果 OOM 了怎么排查? 存放于 Native 内存中。Java 层面的 OOM (OutOfMemoryError) 和 Native 内存溢出不同。排查时可以抓取 hprof,看是不是应用中有大量的 Bitmap 引用没有被回收。线上可以用 KOOM 等库,利用 fork 子进程的方式 dump 内存,减少对主进程的影响。

易错点 / 追问

  • 误区: “使用弱引用可以解决所有内存泄漏”。很多时候泄漏是因为生命周期管理错乱,比如网络请求没取消导致回调还在等。粗暴地改成弱引用可能掩盖逻辑漏洞(回调突然没反应了)。正确的做法是绑定生命周期及时取消任务。
  • 追问: LeakCanary 会影响生产环境的性能吗?(答:会,抓取 hprof 时进程会发生长达数秒的冻结,且分析过程耗 CPU 和内存,所以通常只在 debug 环境或灰度阶段启用。)
  • 易错点: 分析 MAT 时死盯所有引用链。只需关注强引用 (Strong Reference),软/弱/虚引用不会阻止对象被垃圾回收。