OOM(Out of Memory),即“内存溢出”,是当应用程序向系统请求的内存超出了虚拟机(VM)或系统所能分配的最大限制时抛出的一个严重错误(Error
)。在 Android 中,这意味着应用的堆内存(Heap)已经耗尽,无法再为新的对象分配空间。
OOM 会直接导致应用崩溃,是影响应用稳定性的主要因素之一。理解和解决 OOM 问题对于提升应用质量至关重要。
每个 Android 应用都有一个独立的、受限的堆内存上限。这个上限因设备而异,取决于设备的 RAM 大小和制造商的配置。你可以通过 ActivityManager.getMemoryClass()
获取当前设备为应用分配的大致堆内存限制(单位:MB)。
OOM 的根本原因可以归结为两类:内存泄漏(Memory Leaks) 和 内存抖动/不合理使用(Memory Churn / Inefficient Usage)。
内存泄漏是指程序中某些对象已经不再被使用,但由于存在其他正在使用的对象对它们的引用,导致垃圾回收器(GC)无法回收这些无用对象。随着时间的推移,泄漏的对象越积越多,最终耗尽堆内存。
常见的内存泄漏场景包括:
Activity
或其他短生命周期 Context
的引用,那么即使 Activity
已经销毁,也无法被回收。Activity
)更长(例如在后台线程中执行),就会导致外部类无法被回收。Cursor
、FileStream
、Bitmap
、BroadcastReceiver
(未注销)等需要手动关闭或注销的资源,如果忘记调用 close()
或 unregisterReceiver()
,会造成资源句柄和相关内存的泄漏。List
, Map
)中添加对象后,忘记在适当时机移除它们。这类问题并非由“泄漏”引起,而是由于在短时间内创建了大量对象,或加载了单个过大的对象,超出了内存上限。
图片宽度 * 图片高度 * 每个像素占用的字节数
。例如,一张 4000x3000 的 ARGB_8888 图片会占用 4000 * 3000 * 4 = 48MB
内存,极易导致 OOM。SparseArray
替代 HashMap<Integer, Object>
)可以显著减少内存占用。排查 OOM 需要借助专业的工具来分析内存使用情况和定位泄漏源。
在现代 Android Studio 版本中,App Inspection 是进行运行时诊断的首选工具。
View > Tool Windows > App Inspection
)。.hprof
文件。.hprof
文件:
MyLeakActivity
的实例数在你离开该页面后应该为 0。如果大于 0,说明发生了泄漏。LeakCanary 是一个强大的、自动化的内存泄漏检测库。强烈建议在 Debug 版本中集成。
Activity
、Fragment
等组件在销毁后是否被正确回收。如果发现泄漏,它会生成一份包含完整引用链的报告,并以通知的形式展现出来,让你能够轻松定位到泄漏代码。在 build.gradle.kts
(或 build.gradle
) 文件中添加依赖:
// build.gradle.kts
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
LeakCanary 会在应用启动时自动完成初始化,无需额外代码。
定位问题后,需要采取针对性的措施来修复和优化。
避免使用长生命周期的 Context:
Context
且其生命周期与应用无关时(如初始化单例、工具类),应使用 context.applicationContext
。Activity
。使用 WeakReference (弱引用):当需要在一个长生命周期对象中引用一个短生命周期对象(如在异步任务中引用 Activity
)时,使用 WeakReference
。弱引用不会阻止 GC 回收其引用的对象。
优化前(错误示例 - 内存泄漏)
class MyLeakActivity : AppCompatActivity() {
// 错误:非静态内部类会持有 MyLeakActivity 的隐式引用
private val mHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// ... 处理消息 ...
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 发送一个延迟消息,当 Activity 关闭后,消息仍在队列中,
// Handler 实例继续存活,导致 Activity 实例也无法被回收。
mHandler.postDelayed({ /* do something */ }, 10000)
}
}
优化后(正确示例 - 使用静态内部类和弱引用)
class MySafeActivity : AppCompatActivity() {
// 1. 定义一个静态内部类 Handler,它不持有外部类的隐式引用
private class SafeHandler(activity: MySafeActivity) : Handler(Looper.getMainLooper()) {
// 2. 使用弱引用持有 Activity
private val activityReference: WeakReference<MySafeActivity> = WeakReference(activity)
override fun handleMessage(msg: Message) {
// 3. 在使用前,通过 get() 获取 Activity 实例并进行空检查
val activity = activityReference.get()
if (activity == null || activity.isFinishing) {
return // Activity 已被回收,安全退出
}
// 在这里安全地操作 Activity
activity.updateUi()
}
}
private val mHandler = SafeHandler(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mHandler.postDelayed({ /* do something */ }, 10000)
}
fun updateUi() { /* ... */ }
override fun onDestroy() {
super.onDestroy()
// 4. (最佳实践) 在 Activity 销毁时移除所有待处理的消息和回调,避免不必要的工作
mHandler.removeCallbacksAndMessages(null)
}
}
使用静态内部类或 Kotlin 的文件级函数:将需要长时间在后台运行的内部类改为静态内部类,这样它就不会持有外部类的隐式引用。如果需要与外部类交互,可以通过 WeakReference
传递其引用。
及时释放资源:
onDestroy
或适当的生命周期回调中,调用 unregisterReceiver()
。try-with-resources
(Java 7+) 或 Kotlin 的 use
函数来自动关闭 Cursor
, InputStream
等 Closeable
资源。FileInputStream(file).use { stream ->
// 对 stream 进行操作,函数结束时会自动关闭
}
高效加载 Bitmap:
ImageView
的尺寸,加载一个缩小版的图片到内存。使用 BitmapFactory.Options
的 inSampleSize
属性。RGB_565
代替 ARGB_8888
,内存占用能减少一半。使用优化的数据结构:
SparseArray<E>
替代 HashMap<Integer, E>
。SparseBooleanArray
替代 HashMap<Integer, Boolean>
。LongSparseArray<E>
替代 HashMap<Long, E>
。key
进行自动装箱,减少了内存分配。谨慎使用 onDraw
:避免在 onDraw
方法中创建新的对象(如 Paint
, Path
)。这些对象应该在初始化时创建并复用。
使用 largeHeap
属性:如果你的应用确实需要大量内存(如图片编辑、大型游戏),可以在 AndroidManifest.xml
的 <application>
标签中添加 android:largeHeap="true"
。但这只是一个备用方案,治标不治本,首要任务还是优化内存使用。
OOM 是一个复杂但可控的问题。解决 OOM 的最佳实践可以总结为:
Context
和静态引用,及时释放资源。通过系统性的排查和持续的优化,你可以显著提升应用的稳定性,为用户提供更可靠的体验。