第3章 Kotlin特有泄漏模式:匿名内部类 / Coroutine作用域 / Composable闭包
3.1 现象识别
Kotlin 编译器为语言特性生成隐式引用,使以下三类场景成为内存泄漏的“温床”:
| 泄漏模式 | 典型现象 | MAT 对象名特征 |
|---|---|---|
| 匿名内部类(对象表达式) | Activity/Fragment 销毁后堆中仍存在其实例,GC Root → Kotlin 匿名类对象 → 外部类 | OuterClass$1 或 OuterClass$object$... |
| Coroutine 作用域泄漏 | 协程未取消,持有 CoroutineScope → ViewModel/Fragment,导致无法回收 | kotlinx.coroutines.Job、Continuation 链 |
| Composable 闭包捕获 | 重组期间 lambda 捕获 Activity/View 引用,导致 Composable 无法释放 | Composer 内部 closure、ComposableLambda |
判断规则:打开 MAT 的 Histogram,筛选你的 Activity/ViewModel 类名,若存在多个实例且 GC Root 路径包含上述特征对象,则判定为 Kotlin 特有泄漏。
3.2 数据采集
使用 Android Studio 的 Memory Profiler 捕获 Dump,导出为 .hprof 后转换为 MAT 可读格式:
# 转换命令(Android SDK 自带)
hprof-conv -z dump.hprof dump_standard.hprof
在 MAT 中打开后,运行 Leak Suspects 报告,重点关注以下三类 Suspect:
- Suspect 1:
One instance of XxxActivity loaded by …
且 shortest path 包含kotlin.jvm.internal.Reflection或匿名类对象 → 对应第1种模式。 - Suspect 2:
Thread … has a stack frame …
且持有kotlinx.coroutines.CoroutineDispatcher或ContinuationImpl→ 对应第2种模式。 - Suspect 3:
… retained size large but no GC root?
路径中出现ComposableLambda或Recomposer→ 对应第3种模式。
若 Leak Suspects 未直接命中,手工执行 Path to GC Roots → exclude weak/soft/phantom references 遍历。
3.3 原因定位
用 故障树 逐层排查:
- 是否匿名内部类:检查 MAT 中目标对象的 outgoing references。若存在
this$0字段指向外部类,则确认。 - 是否协程作用域:查看
CoroutineScope对象的coroutineContext[Job]是否还处于Active状态(MAT Attributes 面板可看 state)。 - 是否 Composable 闭包:展开
ComposableLambda的_capturedArgs,若包含 Activity/Context 引用,则闭包泄漏。
排查表:常见 MAT 字段与对应模式
| MAT 字段名 | 模式 | 解释 |
|---|---|---|
this$0 | 匿名内部类 | Kotlin 编译器自动生成,指向外部类实例 |
dispatcher | 协程泄漏 | 持有 HandlerDispatcher 或 Executors 的引用 |
composer | Composable | Composable 重建时保留旧 lambda 捕获的引用 |
3.4 修复动作
根据上述定位结果选择修复方案:
- 匿名内部类:将对象表达式改为静态内部类,或使用
WeakReference包装外部引用。例:
// 泄漏写法
val listener = object : OnClickListener {
override fun onClick(v: View) {
// 隐式持有 this@Activity
doSomething()
}
}
// 修复:提取为顶层 lambda 或使用弱引用
val listener = WeakReference(this).let { ref ->
OnClickListener { ref.get()?.doSomething() }
}
onDestroy() 或 onCleared() 中取消 Job:// 在 ViewModel 中应使用 viewModelScope
viewModelScope.launch { ... }
// 如使用自定义 Scope,务必手动取消
private val myScope = CoroutineScope(Dispatchers.IO)
fun destroy() {
myScope.cancel()
}
remember 和 derivedStateOf:@Composable
fun SafeComposable(activity: Activity) {
// 泄漏:lambda 捕获 activity
// 修复:使用 remember 提取不持引用状态
val safeAction = remember { { activity.finish() } }
Button(onClick = safeAction) { Text("退出") }
}
注意:Compse 中
remember 本身也会在重组时重新计算捕获,若捕获对象生命周期长于 Composable,则仍需使用 remember { mutableStateOf } 或 LaunchedEffect 处理。3.5 复测
修复后按以下步骤验证:
- 触发场景(如旋转屏幕、返回退出),重复三次以产生老化对象。
- 抓取 Dump,在 MAT 中打开 Histogram,过滤修复前的目标类名,确认实例数 ≤ 1。
- 如果仍有残留,重新执行 Path to GC Roots 检查是否还有其他隐式引用链。
判断规则:复测通过的标准是 Dump 中目标类实例数不大于正常运行时的存活数(通常为1),且 GC Root 路径不再包含 Kotlin 编译生成的匿名类或
Continuation。3.6 预防
将以下检查项纳入 CI 门禁或代码审查:
| 预防项 | 检测方式 | 执行时机 |
|---|---|---|
对象表达式未被 object : 替代 | Lint 规则 KotlinAnonymousFunction | 编译时 |
自定义 CoroutineScope 缺少 cancel() | 静态代码分析或使用 LeakCanary 2.10+ 的协程检测 | 编译时 / 运行时 |
| Composable lambda 捕获 Activity | 人工审查 + MAT 快照对比 | Code Review |
此外,推荐在基类中统一管理协程生命周期:
abstract class BaseViewModel : ViewModel() {
fun launchCatching(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch { block() }
}
}
避免在 Composable 中直接传入 Activity 引用,改用 LocalContext.current 结合 DisposableEffect 释放。养成在 onPause 或 onStop 中取消协程的习惯。