内存泄漏简介
关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间。
那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢?
这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的 Activity
、Fragment
、fragment View
、ViewModel
如果没有被回收,当前应用被判定为发生了内存泄漏。LeakCanary 会生成引用链相关的日志信息。
有了引用链的日志信息,我们就可以开开心心的解决内存泄漏问题了。但是除了查看引用链还有更好的解决方式吗?答案是有的,那就是通过画图来解决,会更加的直观形象~
一个简单的例子
如下是一个 Handler 发生内存泄露的例子:
kotlin
class MainActivity : ComponentActivity() {
private val handler = LeakHandler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
// other code
// 发送了一个 100s 的延迟消息
handler.sendMessageDelayed(Message.obtain(), 100_000L)
}
private fun doLog() {
Log.d(TAG, "doLog")
}
private inner class LeakHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
doLog()
}
}
}
因为 LeakHandler
是一个内部类,持有了外部类 MainActivity
的引用。
其在如下场景会发生内存泄漏:onCreate
执行之后发送了一个 100s 的延迟消息,在 100s 以内旋转屏幕,MainActivity
进行了重建,上一次的 MainActivity
还被 LeakHandler
持有无法释放,导致内存泄露的产生。
引用链图示
如下是执行完 onCreate()
方法之后的引用链图示:
简单说明一下引用链 0 的位置,这里为了简化,直接使用 GCRoot 代替了,实际上存在这样的引用关系:GCRoot → ActivityThread → Handler → MessageQueue → Message。简单了解一下即可,不太清楚的话也不会影响接下来的问题解决。 同时,为了简单明了,文中还简化了一些相关但是不重要的引用链关系,比如
Handler
对MessageQueue
的引用。
100s 以内旋转屏幕之后,引用链图示变成这样了:
之前的 Activity
被释放,引用链 4 被切断了。我们可以很清晰的看到,由于 LeakHandler
对 MainActivity
的强引用(引用链2),LeakHandler
间接被 GCRoot 节点强引用,导致 MainActivity
没办法释放。
MainActivity
指的是旋转屏幕之前的Activity
,不是旋转屏幕之后新建的
那么很显然,接下来我们就需要对引用链 0、1 或 2 进行一些操作了,这样才可以让 MainActivity
得到释放。
解决方案
方案一:
在 onDestroy()
的时候调用 removeCallbacksAndMessages(null)
,该方法会进行两步操作:移除该 Handler
发送的所有消息,并将 Message
回收到 MessagePool
:
kotlin
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
此时,在图示上的表现,就是移除了引用链 0 和 1。如此 MainActivity
就可以正常回收了。
方案二:
使用弱引用 + 静态内部类的方式,我们同样也可以解决这个内存泄漏问题,想必大家已经非常熟悉了。
这里再简单说明一下弱引用 + 静态内部类的原理: 弱引用的回收机制:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只被弱引用引用 的对象,不管当前内存空间足够与否 ,都会回收它的内存。 静态内部类:静态内部类不会持有外部类的引用,也就不会有 LeakHandler 直接引用 MainActivity 的情况出现
代码实现上,只需要传入 MainActivity
的弱引用给 NoLeakHandler
即可:
kotlin
private val handler = NoLeakHandler(WeakReference(this), Looper.getMainLooper())
private class NoLeakHandler(
private val activity: WeakReference<MainActivity>,
looper: Looper
): Handler(looper) {
override fun handleMessage(msg: Message) {
activity.get()?.doLog()
}
}
下图中,2.1 表示的是 NoLeakHandler
对 WeakReference
的强引用,NoLeakHandler
通过 WeakReference
间接引用到了 MainActivity
。我们可以很清楚的看到,在旋转屏幕之后,MainActivity
此时只被一个弱引用引用了(引用链 2.2,使用虚线表示),是可以正常被回收的。
另一个简单的例子
再来一个静态类持有 Activity 的例子,如下是关键代码:
kotlin
object LeakStaticObject {
val activityCollection: MutableList<Activity> = mutableListOf()
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// other code
activityCollection.add(this)
}
}
正常运行的情况下,存在如下的引用关系:
在旋转屏幕之后,会发生内存泄漏,因为之前的 MainActivity
还被 GCRoot
节点(LeakStaticObject
)引用着。那要怎么解决呢?相信大家已经比较清楚了,要么切断引用链 0,要么将引用链 0 替换成一个弱引用。由于比较简单,这里就不再单独画图说明了。
总结
本文介绍了一种使用画图来解决常见内存泄露的方法,会比直接查看引用链更加清晰具体。同时,其相比于一些归纳常见内存泄漏的方法,会更加的通用,很大程度上摆脱了对内存泄漏场景的强行记忆。
通过画图,找到引用路径之后,在引用链的某个节点上进行操作,切断强引用或者将强引用替换成弱引用,以此来解决问题。
总的来说,对于常见的内存泄漏场景,我们都可以通过画图来解决,本文为了介绍简便,使用了比较简单常见的例子,实际上,遇到复杂的内存泄漏,也可以通过画图的方式来解决。当然,熟练之后,省略画图的操作,也是可以的。
REFERENCE
Excalidraw --- Collaborative whiteboarding made easy