内存泄漏(Memory Leak)——应用程序持有不再需要的对象的引用,阻止垃圾回收器释放 occupied memory 的情况。根据 LeakCanary 的数据,即使在编写良好的应用程序中,每 10,000 行代码也会出现 3–5 个泄漏。每个泄漏都会逐渐减少可用内存,导致运行缓慢和 OutOfMemoryError。
要点
内存泄漏(Memory Leak)——对象通过强引用(Strong Reference)链仍然可达,尽管从逻辑上来说应用程序不再需要它。垃圾回收器(GC)认为这样的对象是存活的,不会释放它占用的内存。结果,可用的堆(Heap)内存不断减少,GC 暂停的频率增加。
与手动管理内存的语言(C, C++)不同,在 Java/Kotlin 中,泄漏不是忘记调用 free(),而是忘记引用。只要存在从根对象(GC Root)到泄漏对象的强引用,GC 就认为它是需要的。典型的 GC Root:静态字段、活动线程、调用栈、JNI 全局引用。
泄漏的危险在于它们的累积效应。一个 100 KB 的泄漏不明显,但 100 个这样的泄漏占用 10 MB,应用程序由于频繁的 GC 而开始变慢。泄漏的临界数量会导致 OutOfMemoryError 和应用程序崩溃。泄漏的症状:Profiler 图表中内存消耗持续增长,频繁的 STW(Stop The World)GC 暂停以及 UI 性能下降。
五种类型的泄漏覆盖了移动开发中 95% 的情况。每种都有自己的原因和代码中的特征模式。
Android 中最著名的泄漏——持有对 Activity 或 Context 的静态引用。典型代码:一个静态的 Activity 字段,在 onDestroy() 时没有置空。只要静态字段存在,整个 Activity 及其可能占用 1–10 MB 的 View 树就会保持存活。这是 LeakCanary 首先发现的经典泄漏。
解决方案:永远不要将 Activity 或 Context 存储在静态字段中。对于比 Activity 生命周期更长的单例,使用 Application Context。如果需要引用 Activity——使用 WeakReference<Activity>。
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
匿名类和非静态内部类隐式持有对包含类的引用。传递给 Handler 并在 onDestroy() 之后执行的 Runnable 会持有整个 Activity。关闭 Activity 的 Retrofit 回调也是如此。这是最隐蔽的泄漏类型——隐式引用在代码中不可见。
Kotlin 的 object 表达式和 Lambda 也会捕获对外部类的引用。将内部类设置为静态(或在 Kotlin 中设为顶层),并通过 WeakReference 传递外部引用。对于 Lambda,使用带有 viewLifecycleOwner 的 Lifecycle-aware 方法。
订阅系统服务而不取消——直接泄漏。在 onResume() 中注册而未在 onPause() 中调用 unregister 的 SensorManager、LocationManager、NotificationListener 会持有 Activity。类似地:未添加到 CompositeDisposable 的 RxJava Disposable 以及通过 GlobalScope 启动的协程。
使用 Lifecycle-aware 组件:带有 LifecycleOwner 的 observe() 会在 onDestroy() 时自动取消订阅。对于 RxJava——带有 DisposableObserver 的 viewLifecycleOwner.lifecycle.addObserver。对于协程——lifecycleScope.launch() 绑定到生命周期。
// 通过 Lifecycle 自动取消订阅
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// 带有 lifecycleScope 的协程
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap 占用大量堆内存:一个 FullHD 位图——1920 × 1080 × 4 字节 = 8.3 MB。如果为列表中的每个元素创建 Bitmap 并且在隐藏时不调用 recycle(),内存会很快耗尽。在旧版 Android(3.0 之前)上,Bitmap 存储在 native 内存中,但在现代版本中——存储在 Dalvik/ART 堆中,GC 只有在没有强引用时才能释放它。
使用 Glide 或 Coil 加载图像——这些库自动管理缓存和回收。如果直接使用 Bitmap,对不再显示的大图像调用 bitmap.recycle(),并使用 inSampleSize 加载缩小的副本。
Fragment 有两个生命周期:Fragment 本身和它的 View。onDestroyView() 之后,View 树被销毁,但如果存在外部引用,Fragment 本身可能保留在内存中。典型错误——在 ViewPager 适配器或导航图中持有对 Fragment 的引用,在销毁时未清理。
永远不要将 对 Fragment 的引用存储在长生命周期对象的字段中。对嵌套 fragment 使用 childFragmentManager,并使用带有 LifecycleOwner 的 observe() 在它们之间传输数据。ViewPager2 在 API 级别解决了这个问题:FragmentTransactionAdapter 正确管理生命周期。
检测泄漏需要验证两个事实:内存在 expected lifetime 后没有返回,并且特定类型的对象数量在不断增长而不减少。诊断过程包括三个阶段。
第一阶段——通过 Android Studio 中的 Memory Profiler 进行视觉检查。打开 Memory 选项卡,执行目标操作(打开和关闭屏幕),按 GC(垃圾回收)并观察内存是否回到初始水平。如果在 3–4 次打开关闭循环后内存持续增长——存在泄漏。
第二阶段——获取 Heap Dump。在 Memory Profiler 中按 Dump Java Heap。将生成的 .hprof 文件在 Android Studio 中打开:你将看到堆中的所有对象及其大小和引用。查找数量在关闭屏幕后应为零的类。例如,关闭后数量为 2 的 MainActivity——明显的泄漏。
第三阶段——分析 Retained Size 和 GC Root。在 Android Studio 中分析 Retained Size:删除此对象将释放多少内存。从 GC Root 到对象的路径显示什么持有它:Static field → HashMap → Activity——你就能看到泄漏点。Reference widget 面板显示对象的所有持有者。
四种工具涵盖了从自动检测到深层 Heap Dump 分析的泄漏查找。
| 工具 | 方法 | 结果格式 |
|---|---|---|
| LeakCanary | 自动监控 | Heap Dump + 泄漏的 stack trace |
| Android Memory Profiler | 手动监控 | 内存图表 + Heap Dump |
| MAT (Eclipse) | 深度分析 | Dominator Tree 报告 + GC Root 路径 |
| Perfetto | 系统级追踪 | 时间线 + native 内存 |
LeakCanary——每个 Android 项目的必备工具。它在 Activity/Fragment 生命周期结束后自动检测泄漏,并显示泄漏的确切位置及 stack trace。集成:build.gradle 中的一行代码。LeakCanary 2.x 不需要手动初始化——它会自动注册 Application Watcher。
预防泄漏通过一套规则和工具嵌入到开发过程中,在每个阶段检查代码。
永远不要将 Activity、Fragment 或 View 的引用存储在静态字段、单例或长生命周期对象中。如果无法避免引用——使用 WeakReference 或通过 ViewModel 存储数据,它正好在需要的时候存活,并且不直接持有 View。
来自 Android Architecture Components 的 ViewModel 和 LiveData 在架构级别解决了生命周期问题。ViewModel 能承受屏幕旋转并且不包含对 View 的引用。LiveData 在 onDestroy() 时自动取消观察者。使用它们代替手动订阅系统服务。
在 代码审查中注意:Context/View 类型的静态字段、匿名类、关闭 Activity 的 Lambda、手动订阅、没有 composite 的 RxJava disposable、通过 Bundle 存储 Fragment。在 Kotlin 中,还要检查协程是否在没有绑定生命周期的情况下启动。
LeakCanary 可以作为测试管道的一部分运行:使用 LeakCanary 运行 acceptance 测试,如果发现泄漏则将构建标记为失败。这可以防止泄漏进入生产环境。用带有 StaticFieldLeak 规则的 Android Lint 补充检查——它在静态分析级别发现潜在泄漏。
// 测试中的 LeakCanary
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // 如果存在泄漏则 fail
}
}
常见问题
泄漏是原因,OutOfMemoryError 是结果。一个泄漏不会导致 OOM,但数十个泄漏的积累会耗尽堆。OOM 是致命异常,而泄漏是一种随时间导致它的模式。
通过 Android Memory Profiler:打开和关闭屏幕 5 次,每次关闭后调用 GC。如果内存没有回到基线水平——存在泄漏。获取 Heap Dump 并在列表中找到关闭后数量大于 0 的 Activity 类。
部分地。Kotlin 解决了空安全的问题,但不管理强引用。带有 lifecycleScope 和 viewModelScope 的协程防止后台任务的泄漏,而 sealed class 和 data class 减少了导致泄漏的状态数量。主要保护——架构模式,而不是语言功能。
LeakCanary 有时会产生误报:对象可能被系统临时持有(例如,InputMethodManager 持有最后一个 View)。手动检查:如果 Retained Size < 1 KB 且 GC Root 是系统服务,这很可能是误报。
不。泄漏可能发生在任何有 GC 的平台上:iOS(Swift/Objective-C)、Flutter(Dart)、Web 浏览器(JavaScript)。机制相同——来自 GC Root 的强引用。在 iOS 上,ARC 自动管理内存,但对象之间的保留循环(retain cycle)造成了同样的泄漏。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。