Android 内存问题通常分两类:一类是对象本来应该被释放却仍被引用,也就是内存泄漏;另一类是对象没有泄漏,但占用过大或释放不及时,导致频繁 GC、卡顿甚至 OOM。
核心结论
- 泄漏本质是生命周期较长的对象持有了生命周期较短的对象。
Activity、Fragment、View、Context是最常见的泄漏对象。Bitmap、大集合、缓存和 Native 资源要主动控制大小和释放时机。onTrimMemory()是系统给应用释放资源的信号,不应该忽略。- 排查时先用 LeakCanary 找引用链,再用 Profiler/MAT 分析大对象和堆占用。
常见泄漏来源
Context 泄漏
不要让单例、静态变量、长生命周期任务持有 Activity Context。
object ImageLoader {
private lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext
}
}
Handler 和回调泄漏
如果延迟任务持有 View 或 Activity,页面销毁后任务仍在队列中,就可能导致泄漏。
处理方式:
- 在
onDestroy()/onDestroyView()移除回调; - 避免匿名内部类长期持有页面;
- 协程场景使用生命周期绑定的 scope。
override fun onDestroyView() {
handler.removeCallbacksAndMessages(null)
super.onDestroyView()
}
注册未反注册
常见对象:
BroadcastReceiver- 监听器
- EventBus / Flow collector
- Sensor callback
- Location callback
原则:在哪里注册,就要有清晰的反注册位置。
大对象和缓存
Bitmap
Bitmap 是 Android 内存问题高发点。
建议:
- 使用 Glide/Coil 这类成熟图片库;
- 按目标尺寸加载,不加载原图;
- 避免在列表中持有大量 Bitmap;
- 大图预览要关注采样率和生命周期。
缓存
缓存不是越大越好。缓存应该有上限、淘汰策略和内存压力响应。
常用策略:
- 内存缓存用
LruCache; - 磁盘缓存用成熟库或明确目录;
- 收到
onTrimMemory()时主动降级或清理; - 不把页面对象放进全局缓存。
onTrimMemory
onTrimMemory(level) 表示系统希望应用释放一部分内存。
常见处理:
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= TRIM_MEMORY_RUNNING_LOW) {
imageCache.trimToSize(imageCache.size() / 2)
}
}
需要关注的信号:
| level | 含义 | 建议 |
|---|---|---|
TRIM_MEMORY_UI_HIDDEN | UI 不可见 | 释放 UI 相关缓存 |
TRIM_MEMORY_RUNNING_LOW | 运行中内存偏低 | 降低缓存 |
TRIM_MEMORY_RUNNING_CRITICAL | 内存非常紧张 | 尽可能释放非必要资源 |
排查工具
LeakCanary
适合自动发现 Activity、Fragment、ViewModel 等对象泄漏。
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.x")
重点看:
- 泄漏对象是什么;
- GC Root 到泄漏对象的引用链;
- 哪个长生命周期对象持有了它。
Android Studio Profiler
适合观察:
- 堆内存变化;
- GC 频率;
- 对象分配热点;
- Heap Dump。
MAT
适合分析复杂堆快照:
- Dominator Tree 看谁占用最大;
- Path to GC Roots 看为什么无法释放;
- Histogram 看对象数量是否异常。
回看清单
- 生命周期长的对象不要持有生命周期短的对象。
- 页面销毁时清理 Handler、监听器、Receiver 和异步任务。
- 大图和缓存必须有大小上限。
- 系统内存压力回调要释放非必要资源。
- 泄漏看引用链,OOM 看对象占用和分配趋势。