0Pricing
Android Academy · 课时

内存泄漏与修复

发现并阻止内存泄漏

内存泄漏与修复 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 衡量性能
  2. 驾驭重组
  3. 内存泄漏与修复
  4. 启动与基线配置文件
← 返回 Android Academy