内存泄漏 (memory leak) — 应用程序未能释放不再需要的对象所占用的内存的情况。在移动开发中,这尤其关键:有限的堆和缺乏交换导致 OutOfMemoryError 和应用程序崩溃。根据 Purdue University(2022年)的数据,Google Play 中 35% 的 Android 应用程序至少包含一个内存泄漏。我们将分析典型场景、诊断工具和修复方法。
要点
内存泄漏 — 是指在对象不再被程序需要后,已分配的内存未能归还给系统的情况。垃圾回收器认为这样的对象是活动的,因为从 GC Root 到该对象有一条活跃的引用链。
在 Java/Kotlin 中,垃圾回收器自动工作,但如果存在技术上的引用,它无法确定对象在逻辑上不再需要。开发人员必须显式中断不需要的连接。在 Swift/Objective-C 中,ARC 自动计数引用,但保留循环会阻止计数器归零。
泄漏的主要危险是 累积效应。每次泄漏消耗少量内存,但在多次屏幕切换(屏幕旋转、打开/关闭 Activity)时,泄漏会不断累积,直到耗尽堆的限制。
泄漏 — 对象对代码不可访问,但未被 GC 删除。膨胀 — 对象在逻辑上是需要的,但以过大的数量存储。膨胀的例子:100 MB 的图像缓存,工作集为 30 MB。两个问题都会导致 OOM,但原因和处理方法不同。
ART(Android Runtime)使用带并发压缩的分代垃圾回收。内存分为新生代(Young)、老年代(Old)和大对象(Large)。经历了多次 GC 周期的对象被移动到老年代,在该区域回收频率较低 — 这加快了常规周期。
当 堆达到一定的占用阈值(通常为 75-85%)时,GC 启动。在 GC 期间,应用程序的所有线程都会被暂停(STW — Stop The World)。活动对象越多,暂停时间越长。泄漏增加了活动对象的数量,从而延长了 GC 暂停时间。
回收器通过从 GC Roots 遍历图来确定活动对象:静态字段、活动线程的栈变量、JNI 引用。从这些根节点通过引用可达的任何对象都被视为活动的 — 即使开发人员知道它不再需要。
// 示例:作为 GC Root 的静态集合 — 永久泄漏
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference 不阻止 GC — 正确行为
}
}
WeakReference 解决了这个问题:GC 在确定活动对象时忽略弱引用。如果只剩下弱引用指向一个对象,它将在下一个 GC 周期中被回收。
Activity Context — Android 中最常见的泄漏场景。如果单例、静态字段或长生命周期服务持有 Activity Context 的引用,则整个 Activity 及其所有 View 都无法被 GC 回收。解决方案:对于长生命周期对象使用 Application Context。
Handler 和已发送的消息 — Handler.postDelayed(runnable, delay) 将消息放入 Main Looper 的队列中。如果 Activity 在延迟到期前被销毁,消息仍在队列中,并通过 Runnable → 匿名类 → 外部类(Activity)持有引用。
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // 必需:清除队列
super.onPause()
}
}
内部类 — 非静态内部类持有对外部类实例的隐式引用。如果外部类是 Activity,且内部类被传递到外部(例如,传递给 RecyclerView.Adapter),则 Activity 无法被回收。
Android Studio Memory Profiler — 用于实时监控堆的内置工具。显示已用内存的图表、按类型分配的数目和对象数量。允许记录堆转储并导出为 HPROF 格式,以便在 MAT 中进行分析。
Eclipse MAT(Memory Analyzer Tool)— 桌面堆转储分析器。自动构建泄漏嫌疑人报告,突出显示保留集最大的对象,并为每个可疑对象建议可能的 GC 根链。
Xcode Memory Graph Debugger — 用于 iOS。暂停应用程序并可视化对象图。保留循环以红色突出显示,可以点击任何对象查看其保留计数和引用。
| 工具 | 功能 | 复杂度 |
|---|---|---|
| Memory Profiler | 实时图表、堆转储、对象分配跟踪 | 低 |
| Eclipse MAT | 支配树、泄漏嫌疑人、OQL 查询 | 中 |
| LeakCanary | 自动检测、通知中的泄漏跟踪 | 最低 |
| Xcode Memory Graph | 保留循环的可视化图、活动对象列表 | 低 |
根据 Uber Engineering Blog,在 CI/CD 流水线中引入自动内存性能分析(LeakCanary + 堆转储分析)可在 3 个月内将生产环境中的内存相关事故减少 60%。
替换 Context — 如果对象比 Activity 存活更久,请使用 applicationContext。所有长生命周期对象(单例、存储库、数据库辅助类)应接收 Application Context,而不是 Activity Context。例外:需要访问 Activity 特定主题或资源的 UI 组件。
Lifecycle-aware 组件 — 使用 LifecycleObserver、DefaultLifecycleObserver 或 reactivex 会在 onDestroy 时自动取消订阅。Android Jetpack 提供了 lifecycleScope 和 viewModelScope,它们会在相应事件发生时被清理。
静态内部类 — 如果内部类不需要访问外部类的字段,请将其设为 static。静态内部类不持有对外部类的隐式引用。如果需要访问,请使用 WeakReference 进行显式引用。
class MyActivity : AppCompatActivity() {
// ❌ 非静态内部类 — 对 MyActivity 的隐式引用
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ 静态内部类 — 无隐式引用
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
在 iOS 中,在闭包中使用捕获列表:[weak self],用于可能比创建者存活更久的闭包。对于委托,使用弱引用(weak var delegate)。对于保证只在 self 生命周期内被调用的闭包,可以使用 [unowned self],但要小心 — 访问已释放的对象会导致崩溃。
常见问题
在 Android 中,多次在屏幕之间切换(Activity A → B → A → B),然后检查 adb shell dumpsys meminfo package_name。如果 Total PSS 持续增长且不返回到初始值 — 说明存在泄漏。在 iOS 中类似:在 Xcode 中使用 Debug Memory Graph 进行可视化检查。
会,如果 CoroutineScope 在组件销毁时没有被取消。在 GlobalScope 中启动的协程即使在 Activity 的 finish() 之后也会继续执行。解决方案:使用 viewModelScope(在 onCleared 中取消)或 lifecycleScope(在 onDestroy 中取消)。对于自定义 Scope,通过 LifecycleOwner 创建生命周期感知的作用域。
Bitmap 将像素数据存储在原生堆中,而不是 Java 堆中。这意味着 Java GC 看不到 Bitmap 的实际大小。如果不调用 Bitmap 的 recycle() 或不将引用置空,原生内存将不会被释放。使用带有 inSampleSize 的 BitmapFactory 加载缩略图,并使用 Glide/Coil 进行自动缓存管理。
静态字段 — 是一个 GC Root。它只要类被加载就存在(在 Android 中 — 只要 Process 存活)。如果静态字段引用了 Activity、Bitmap、View 或任何其他重量级对象,该对象将永远不会被 GC 回收。静态字段 — 永久的引用。解决方案:只存储 WeakReference 或在 onDestroy 中将静态字段置空。
ARC 在强引用计数降至零时自动释放对象。保留循环是 ARC 中唯一的泄漏方式。始终对 parent→child 引用使用 weak,其中 child 应该比 parent 存活更久(委托、数据源)。对于闭包,使用捕获列表 [weak self],并在闭包内部检查 self 是否为 nil。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。