内存耗尽和膨胀 — 是什么、原因及如何避免

作者: IT Sectr 发布日期: 2026-07-28 阅读时间: 10 分钟

内存泄漏 — 移动开发中最隐蔽的问题之一。应用程序的内存不断增长,直到达到操作系统设定的限制,随后出现 OutOfMemoryError 或强制终止。根据 Square Engineering 的数据,大约 40% 的 Android 应用 至少有一个内存泄漏,且只能在性能分析时被发现。我们来分析内存增长的原因和预防方法。

要点

  • GC 可达性 — 如果对象有来自根集合的活动引用,则不会被删除
  • 对 Activity 或 Context 的静态引用 — Android 中最常见的泄漏原因
  • LeakCanary — Android 中自动检测泄漏的标准工具
  • WeakReference — 用于不应阻止垃圾回收的引用的解决方案
  • Lifecycle-aware 组件 在视图销毁时自动取消订阅

什么是内存泄漏和应用程序膨胀?

内存泄漏(memory leak)— 应用程序不再需要的对象由于存在来自根集合(GC Root)的活动引用而继续保留在堆中的情况。垃圾收集器认为此类对象是存活的,不会将其删除。

内存膨胀(memory bloat)— 一个更广泛的问题,应用程序消耗的内存超出执行当前任务所需。原因包括:过度缓存、对象重复、非最优的数据结构以及堆碎片化。

Android 中,每个应用程序分配有限的堆(通常为 64-512 MB,取决于设备和操作系统版本)。在 iOS 中,限制较少严格,但系统在接近限制时会发送内存警告。

特性AndroidiOS
堆限制64-512 MB(取决于设备)隐式(系统)
垃圾收集ART(Concurrent, Compact)ARC(自动引用计数)
泄漏机制GC Root 引用保留循环(强引用循环)
结果OutOfMemoryError内存警告 → 终止

根据 Facebook Engineering Blog,内存泄漏是移动应用中约 15% 的崩溃报告的原因。在 Android 中,由于内存不足时频繁的 GC 暂停,还会出现 ANR

Android 和 iOS 中典型的内存泄漏模式

对 Activity 的静态引用 — Android 泄漏的经典案例。如果静态字段或单例持有对 Activity 的引用,即使调用 finish() 后,只要单例存活,GC 也不会回收该 Activity。Activity 是一个包含视图层次结构、资源和 Context 的重型对象。

kotlin
object LeakHolder {
    var activityRef: Activity ?= null // 泄漏:对 Activity 的静态引用
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        LeakHolder.activityRef = this // ❌ MainActivity 永远不会被 GC 回收
    }
}

匿名类和 lambda — 隐式持有对外部类的引用。如果将 Runnable 或 Callback 传递给外部服务,并且 Activity 被销毁,匿名类对象仍然挂在队列中,阻止 Activity 被垃圾回收。

  • 带延迟的 Handler — 如果 Activity 已被销毁,但 Handler.postDelayed 尚未执行,则 Activity 泄漏
  • Thread 和 AsyncTask — 屏幕旋转时 Activity 重新创建,但旧线程仍持有对旧 Activity 的引用
  • Retrofit/Callback — 匿名 Callback 持有对 presenter 或 fragment 的引用
  • 观察者(Observers) — 未在 onDestroy 时取消的 LiveData 或 RxJava 订阅

iOS 中,主要问题是 保留循环(retain cycles):两个对象相互持有强引用,ARC 无法将任一对象的引用计数归零。典型情况:闭包强捕获 self,而 self 持有对闭包的引用。

如何检测内存泄漏?

LeakCanary — Square 开发的用于 Android 自动检测泄漏的库。在 Activity 或 Fragment 销毁后,检查对象是否被 GC 回收。如果没有,则进行堆转储并显示泄漏跟踪。

kotlin
// LeakCanary 2.x — 通过 Application 自动集成
class ExampleApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // LeakCanary 在调试版本中自动安装
        // 通过 ContentProvider — 零代码设置
    }
}

// 强制检查调用
AppWatcher.objectWatcher.watch(watchedObject, "leak description")

Android Studio Profiler — 用于实时监控内存的内置工具。可以录制堆转储、查找可疑对象(保留大小 > 1 MB)并跟踪到每个对象的 GC 根路径。

对于 iOS,使用 Xcode Memory Graph Debugger。它可以可视化内存中的对象图,显示保留循环,并允许立即检测循环引用。还提供 Instruments > Allocations 用于长期监控。

防止泄漏的策略

WeakReference — 用于不应阻止垃圾回收的引用的基本机制。如果 GC 决定删除该对象,WeakReference 返回 null。用于回调、监听器以及从后台线程对 UI 组件的引用。

Lifecycle-aware 组件 — Android Jetpack(Lifecycle, LiveData, Flow, coroutines)中实现的架构方法。订阅在 onDestroy 时自动取消,从而消除了主要的泄漏类别。

kotlin
class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<List<User>>()
    val data: LiveData<List<User>> get() = _data

    fun loadData() {
        viewModelScope.launch {
            val result = repository.fetchData()
            _data.postValue(result)
            // 协程在 onCleared() 时自动取消
        }
    }
}

viewModelScopelifecycleScope — Android 中内置的 CoroutineScope,在相应的生命周期事件发生时取消。这消除了通过协程的泄漏 — 现代 Android 开发中最常见的场景。

  • 不要使用 对 Context、Activity、View 或 Fragment 的静态引用
  • 在 onDestroy 时 通过 disposeBag / CompositeDisposable 取消所有 RxJava 订阅
  • 在 iOS 闭包中使用 [weak self] / [unowned self] 以防止保留循环
  • 检查 Bitmap 和大对象 — 它们应该被回收或置空

内存分析工具

Android Studio 中的 Memory Profiler — 监控堆的主要工具。显示实时分配、堆快照、按类型的对象数量。可以录制转储并在 MAT(Memory Analyzer Tool) 中分析以查找可疑对象。

Eclipse MAT — 桌面堆转储分析器。从 Android Studio 加载 HPROF 文件后,MAT 构建支配树,显示每个对象的保留大小,并通过 Leak Suspects Report 提供可疑泄漏的自动分析。

Xcode Memory Graph — 用于保留循环的可视化调试器。点击 Memory Graph Debugger 按钮后,Xcode 停止应用程序,构建内存中对象的完整图,并用红色高亮显示保留循环。

工具平台特点
LeakCanaryAndroid销毁后自动检测泄漏
Memory ProfilerAndroid Studio堆转储 + 实时分配
Eclipse MATAndroid支配树、Leak Suspects Report
Memory GraphiOS(Xcode)保留循环可视化器

根据 Google I/O 2023,在调试版本中使用 LeakCanary 的应用程序在实施后的前 2 个月内将内存相关崩溃数量减少了 30-50%。建议在项目入门阶段添加 LeakCanary。

常见问题

内存泄漏和膨胀有什么区别?

泄漏 — 代码无法访问但由于活动引用而未被 GC 删除的对象。膨胀 — 应用程序在内存中保留逻辑上需要但数量过多的对象(例如,80 MB 运行中的应用程序中 50 MB 的缓存)。膨胀通过架构方式解决,泄漏则通过正确的引用管理来解决。

LeakCanary 如何发现泄漏?

LeakCanary 使用 ObjectWatcher — 在 onDestroy() Activity 后,它创建对 Activity 的 WeakReference 并运行 GC。如果 5 秒后 WeakReference 未被清除,LeakCanary 进行堆转储,分析从 GC Root 到对象的最短引用链,并显示精确的泄漏堆栈,包括文件名和代码行。

为什么 Bitmap 经常导致 OutOfMemoryError?

Bitmap 在 Java 堆之外的原生内存中占用空间。一个 Bitmap 的大小 = 宽 × 高 × 4 字节(ARGB_8888)。一张 12 MP 的照片(4000×3000)占用 48 MB。Android 并不总是能够及时释放原生内存,当多个 Bitmap 累积时,即使 Java 堆足够也会导致 OOM。

iOS 中的保留循环是什么?

保留循环 — ARC 中两个对象相互持有强引用且引用计数永远无法归零的情况。典型示例:ViewController 持有对闭包的强引用,而闭包强捕获 self。解决方案:在闭包中使用 [weak self][unowned self]

Android 中堆的最大大小是多少?

的大小取决于设备和 Android 版本。对于旧设备(API 15-24)— 64-128 MB。对于现代设备(API 25+)— 256-512 MB。精确值可以通过 ActivityManager.getMemoryClass() 获取。对于大型应用程序(游戏、编辑器),manifest 中有 largeHeap=true,可提供高达 1 GB 的空间。

总结

  • 内存泄漏 — 由于来自根集合的活动引用而未被 GC 删除的对象;膨胀 — 没有明显泄漏的过度内存消耗
  • 对 Activity、Context、View 的静态引用 — Android 中泄漏的首要原因;解决方案 — WeakReference 或 Application Context
  • 匿名类和 lambda 隐式持有对外部类的引用;未取消的回调 — 第二常见的原因
  • LeakCanary — Android 中自动泄漏检测的标准;集成只需 5 分钟,可将崩溃率降低 30-50%
  • lifecycleScope 和 viewModelScope 在销毁时自动取消协程,消除了整个泄漏类别
  • iOS 中的 保留循环 通过在闭包和委托中使用 weak/unowned self 解决
  • 定期分析内存 每个 sprint 至少一次 — 使用 MAT 或 Memory Graph 进行堆转储应成为代码审查的一部分

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读