Android 内存泄漏的根源与排查
内存泄漏的本质
Android 使用 Java 的垃圾回收机制(GC):当一个对象不再被任何GC Roots(活跃线程、static 变量、JNI 引用等)引用时,GC 会将其回收。
内存泄漏 = 应该被回收的对象,因为意外被长生命周期对象持有,导致无法回收。
Activity 是最常见的泄漏对象:它关联着整个界面的 View 树(可能几兆内存),一旦 Activity 被意外持有,这片内存就永远无法释放。每次用户旋转屏幕(系统重建 Activity),都会泄漏一个新的 Activity 实例,最终 OOM。
五种经典泄漏场景
1. 静态变量持有 Context
// ❌ 危险!singleton 是静态的,生命周期等于进程
object ImageCache {
var context: Context? = null // 如果 context 是 Activity,它永远不会被回收
}
// 使用时
ImageCache.context = this // this 是 Activity!
// ✓ 正确:存 Application Context
object ImageCache {
lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext // applicationContext 生命周期 = 进程
}
}
规则:静态变量需要 Context 时,只能用 applicationContext,永远不要存 Activity/Fragment/View。
2. 非静态内部类(最隐蔽)
class MainActivity : Activity() {
// ❌ 匿名类 / Handler 内部类隐式持有外部类(MainActivity)的引用
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// 这里的 this 可以访问 MainActivity 的成员
// Handler 内部隐式持有 MainActivity 引用
}
}
fun postDelayed() {
// 如果发送了延迟消息,ActivityA 关闭了,但消息还在队列中
// MainActivity 无法被 GC,因为 MessageQueue → Handler → MainActivity
handler.postDelayed({ doSomething() }, 30_000) // 30秒后执行
}
}
// ✓ 正确:静态内部类 + 弱引用
class MyHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
private val weakActivity = WeakReference(activity) // 弱引用,不阻止 GC
override fun handleMessage(msg: Message) {
val activity = weakActivity.get() ?: return // Activity 已销毁,直接返回
activity.doSomething()
}
}
规则:Handler、Runnable、匿名类等,如果生命周期比 Activity 长,必须使用静态类 + 弱引用。
3. 监听器/回调未注销
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ❌ 注册了全局监听器,但在 Fragment 销毁时没有注销
EventBus.getDefault().register(this)
locationManager.requestLocationUpdates(provider, 0, 0f, locationListener)
SomeGlobalManager.addListener(this)
}
// Fragment 销毁时,全局 Manager 还持有对 Fragment 的引用!
override fun onDestroyView() {
super.onDestroyView()
// ✓ 配对注销
EventBus.getDefault().unregister(this)
locationManager.removeUpdates(locationListener)
SomeGlobalManager.removeListener(this)
}
}
4. 集合类累积
object Cache {
// ❌ 只 put,从不 remove,内存无限增长(非严格意义的泄漏,但效果相同)
val cache = HashMap<String, Bitmap>()
fun addBitmap(key: String, bitmap: Bitmap) {
cache[key] = bitmap
}
}
// ✓ 使用 LruCache(最近最少使用策略,自动淘汰旧数据)
val imageCache = LruCache<String, Bitmap>(maxSize = 50 * 1024 * 1024) // 50MB
// 或用弱引用 Map,允许 GC 回收 value
val weakCache = WeakHashMap<String, Bitmap>()
5. Bitmap 没回收(API < Android O)
Android O(8.0)之前,Bitmap 内存在 Java Heap 中,需要手动 recycle()。8.0 之后 Bitmap 内存移到 Native Heap,GC 可以自动管理,但仍需避免长时间持有不需要的大 Bitmap 引用。
弱引用、软引用、虚引用
Java 提供四种引用类型,用于精细控制 GC 行为:
| 引用类型 | GC 行为 | 典型用途 |
|---|---|---|
| 强引用(普通赋值) | 永不回收 | 正常使用 |
SoftReference<T> |
内存不足时回收 | 图片缓存(内存足够就保留) |
WeakReference<T> |
每次 GC 都会回收 | Handler 持有 Activity、Context 传递 |
PhantomReference<T> |
对象被回收时通知 | 资源清理、finalize 替代 |
// WeakReference 正确用法
class Presenter {
private var weakView: WeakReference<View>? = null
fun attachView(view: View) {
weakView = WeakReference(view)
}
fun updateUI(data: Data) {
val view = weakView?.get() ?: return // View 已被销毁,get() 返回 null
view.display(data) // 安全使用
}
}
LeakCanary:自动检测内存泄漏
LeakCanary 是 Square 开源的内存泄漏检测库,接入极简:
// build.gradle.kts
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}
不需要任何初始化代码(2.x 版本通过 ContentProvider 自动初始化)。
LeakCanary 的工作原理:
- 通过
ActivityLifecycleCallbacks监控所有 Activity 的onDestroy - Activity 销毁后,把它放入
WeakReference,然后触发 GC - 等待 5 秒后,检查 WeakReference 是否被清空
- 如果对象还活着(WeakReference.get() != null),怀疑泄漏
- 触发 Heap Dump(
.hprof文件),分析引用链,找出 GC Root → 泄漏对象的路径 - 显示通知,告知开发者泄漏的具体路径
LeakCanary 报告示例:
┬───
│ GC Root: Local variable in thread main
│
├─ android.os.MessageQueue
│ Leaking: NO (MessageQueue is a GC root)
│
├─ com.example.MyHandler
│ Leaking: UNKNOWN
│
╰→ com.example.MainActivity
Leaking: YES (Activity#mDestroyed is true)
从报告里可以直接看出:MyHandler 持有了已销毁的 MainActivity。
用 Android Studio Profiler 排查
-
Memory Profiler:实时查看内存折线图,操作 App 时观察内存是否持续增长(锯齿状是正常的,但基线不断抬高说明泄漏)
-
Heap Dump:点击"Capture heap dump",分析当前堆内存:
- 按类名过滤 Activity,如果存在多个同类 Activity 实例,说明泄漏
- 查看 Retained Size(该对象被回收后能释放多少内存)
-
Allocation Tracker:记录一段时间内的对象分配,找出频繁分配的对象
协程与 ViewModel 防泄漏
在 Jetpack 时代,很多泄漏问题有了更优雅的解决方案:
// ✓ viewModelScope:ViewModel 销毁时自动取消所有协程
class MyViewModel : ViewModel() {
fun loadData() = viewModelScope.launch {
// 协程绑定到 ViewModel 生命周期,Activity 销毁 → ViewModel 销毁 → 协程取消
}
}
// ✓ lifecycleScope:跟随 Activity/Fragment 生命周期
class MyFragment : Fragment() {
override fun onViewCreated(...) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 只在 STARTED 以上状态收集,进后台自动暂停,不泄漏
viewModel.state.collect { updateUI(it) }
}
}
}
}
协程取消时,会在挂起点抛出 CancellationException,协程自然结束,不会泄漏。这是现代 Android 开发防泄漏的主要手段。