内存泄漏与修复
发现并阻止内存泄漏
内存泄漏与修复 是 CoddyKit 上的免费 Android Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。
什么是内存泄漏
当某些对象已经不再需要,却因为仍有其他对象持有它们的引用而无法被垃圾回收时,就会发生内存泄漏。在 Android 中,最典型的受害者是屏幕销毁后仍然残留的 Activity 或 Context。
泄漏会让堆内存随着时间增长,触发更频繁的垃圾回收(这会暂停界面并导致卡顿),最终还可能使应用因 OutOfMemoryError 崩溃。本课程将展示如何查找和修复泄漏。
垃圾回收器如何决定保留什么
Android 的垃圾回收器会保留所有从 GC 根(活动线程、静态字段等)可达的对象。不可达的对象会被释放。
泄漏本质上就是一条不应存在的可达路径:一个生命周期很长的对象持有了生命周期很短的对象的引用。下面的纯 Kotlin 示例展示了可达性如何让对象保持存活。
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}经典案例:Activity 泄漏
最常见的 Android 泄漏,是静态对象或长期存活的对象持有 Context。当 Activity 被销毁时(例如旋转屏幕或导航离开),系统本想回收它,但静态引用让它永远无法释放。
下面的代码会造成泄漏:单例缓存了 Activity 的上下文。
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}修复:使用 Application Context
当您必须在长期存活的对象中保存 Context 时,请保存应用上下文。它的存活期与整个进程相同,可以安全地长期持有。
只有在对应屏幕存在期间,才保留 Activity/View 上下文。遵守这一条规则,就能避免大多数 Android 泄漏。
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activity内部类与 Handler
非静态内部类(包括大多数监听器、Runnable 和 Handler 回调)都会持有外部类的隐式引用。如果这项工作比屏幕存活得更久,就会导致屏幕泄漏。
延迟执行的 Handler.postDelayed 是常见元凶:待处理的消息会让 Activity 一直存活到消息触发。
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}协程:为工作限定作用域
存活时间超过所属屏幕的协程,会泄漏它捕获的所有内容。解决方法是使用结构化并发:在与生命周期绑定的作用域中启动工作,使其能够自动取消。
在 ViewModel 中使用 viewModelScope;在界面控制器中使用 lifecycleScope。所有者销毁后,作用域会取消,引用也会被释放。
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()Compose 中的 DisposableEffect
在 Compose 中,凡是您注册的内容,都必须在可组合项离开时取消注册。DisposableEffect 正好提供了用于此目的的 onDispose 代码块:可用于监听器、观察者、传感器和广播接收器。
如果忘记在这里清理,就会泄漏回调及其捕获的所有内容。
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}使用 LeakCanary 查找泄漏
LeakCanary 是在调试构建中自动捕获泄漏的标准工具。添加依赖后,它会监视已销毁的对象;如果某个对象没有被垃圾回收,它会转储堆并显示使该对象保持存活的确切引用链。
这条链就是修复线索:它会直接指向有问题的字段或监听器。
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)分析器中的堆转储
对于 LeakCanary 未标记的泄漏(缓慢增长、原生内存、大型缓存),请使用内存分析器。捕获堆转储,强制执行 GC,然后检查哪些类的实例数量异常多。
离开页面后,如果您的 Activity、Fragment 或 ViewModel 的实例数量仍在增长,这是明显的泄漏信号。分析器还会显示通往 GC 根的路径,帮助您追踪引用。
位图与大型内存分配
并非所有内存问题都是引用泄漏。大型位图和无上限的缓存本身就可能耗尽堆内存。一张全分辨率照片在内存中可能占用数十兆字节。
让图像库(Coil/Glide)将图像缩小到显示尺寸,并为您创建的任何缓存设置明确的大小上限。下面的代码片段展示了一个有大小限制的 LRU 缓存。
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.防止泄漏检查清单
养成以下习惯,就能避免大多数泄漏:
- 在长期存活的对象中存储 applicationContext,绝不要存储 Activity/View。
- 在受生命周期限定的协程中运行异步工作(
viewModelScope、lifecycleScope)。 - 始终取消注册监听器和接收器(
onDispose、onDestroy)。 - 取消延迟执行的 Handler 和计时器。
- 为缓存设置大小上限,并缩小位图。
- 在调试构建中保留 LeakCanary,并根据它提供的追踪信息采取行动。
快速检查
您需要让一个单例对象在整个生命周期内保存 Context。为了避免泄漏屏幕,它应该持有什么上下文?
回顾:保持内存干净
您已经了解了什么是泄漏,以及如何阻止泄漏:
- 泄漏是指不需要的可达性路径,让已失效的对象继续存活。
- 经典泄漏是对 Activity/Context 的静态或长期存活引用——应改为存储 applicationContext。
- 内部类监听器、延迟执行的 Handlers 和未限定作用域的协程会泄漏其所有者——请为它们限定作用域并取消它们。
- 在 Compose 中使用
DisposableEffect取消注册回调。 - LeakCanary 和内存分析器可以揭示引用链。
- 为缓存设置大小上限,并缩小位图。
下一步:通过启动优化和基线配置文件,让应用快速启动。
常见问题解答
「内存泄漏与修复」课时是免费的吗?
是的 — 「内存泄漏与修复」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。
「内存泄漏与修复」这节课中我会学到什么?
发现并阻止内存泄漏 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「内存泄漏与修复」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。