注册

【内存泄漏】图解 Android 内存泄漏

内存泄漏简介


关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间


那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢?


这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的 ActivityFragment 、fragment ViewViewModel 如果没有被回收,当前应用被判定为发生了内存泄漏。LeakCanary 会生成引用链相关的日志信息。


有了引用链的日志信息,我们就可以开开心心的解决内存泄漏问题了。但是除了查看引用链还有更好的解决方式吗?答案是有的,那就是通过画图来解决,会更加的直观形象~


一个简单的例子


如下是一个 Handler 发生内存泄露的例子:


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() 方法之后的引用链图示:


memory_leak_1.png



简单说明一下引用链 0 的位置,这里为了简化,直接使用 GCRoot 代替了,实际上存在这样的引用关系:GCRoot → ActivityThread → Handler → MessageQueue → Message。简单了解一下即可,不太清楚的话也不会影响接下来的问题解决。
同时,为了简单明了,文中还简化了一些相关但是不重要的引用链关系,比如 HandlerMessageQueue 的引用。



100s 以内旋转屏幕之后,引用链图示变成这样了:


memory_leak_2.png


之前的 Activity 被释放,引用链 4 被切断了。我们可以很清晰的看到,由于 LeakHandlerMainActivity 的强引用(引用链2),LeakHandler 间接被 GCRoot 节点强引用,导致 MainActivity 没办法释放。



MainActivity 指的是旋转屏幕之前的 Activity,不是旋转屏幕之后新建的



那么很显然,接下来我们就需要对引用链 0、1 或 2 进行一些操作了,这样才可以让 MainActivity 得到释放。


解决方案


方案一:


onDestroy() 的时候调用 removeCallbacksAndMessages(null) ,该方法会进行两步操作:移除该 Handler 发送的所有消息,并将 Message 回收到 MessagePool


override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}

此时,在图示上的表现,就是移除了引用链 0 和 1。如此 MainActivity 就可以正常回收了。


memory_leak_3.png


方案二:


使用弱引用 + 静态内部类的方式,我们同样也可以解决这个内存泄漏问题,想必大家已经非常熟悉了。



这里再简单说明一下弱引用 + 静态内部类的原理:
弱引用的回收机制:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只被弱引用引用的对象,不管当前内存空间足够与否,都会回收它的内存。
静态内部类:静态内部类不会持有外部类的引用,也就不会有 LeakHandler 直接引用 MainActivity 的情况出现



代码实现上,只需要传入 MainActivity 的弱引用给 NoLeakHandler 即可:


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 表示的是 NoLeakHandlerWeakReference 的强引用,NoLeakHandler 通过 WeakReference 间接引用到了 MainActivity。我们可以很清楚的看到,在旋转屏幕之后,MainActivity 此时只被一个弱引用引用了(引用链 2.2,使用虚线表示),是可以正常被回收的。


memory_leak_4.png


另一个简单的例子


再来一个静态类持有 Activity 的例子,如下是关键代码:


object LeakStaticObject {
val activityCollection: MutableList<Activity> = mutableListOf()
}

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
// other code

activityCollection.add(this)
}
}

正常运行的情况下,存在如下的引用关系:


memory_leak_5.png


在旋转屏幕之后,会发生内存泄漏,因为之前的 MainActivity 还被 GCRoot 节点(LeakStaticObject)引用着。那要怎么解决呢?相信大家已经比较清楚了,要么切断引用链 0,要么将引用链 0 替换成一个弱引用。由于比较简单,这里就不再单独画图说明了。


总结


本文介绍了一种使用画图来解决常见内存泄露的方法,会比直接查看引用链更加清晰具体。同时,其相比于一些归纳常见内存泄漏的方法,会更加的通用,很大程度上摆脱了对内存泄漏场景的强行记忆。


通过画图,找到引用路径之后,在引用链的某个节点上进行操作,切断强引用或者将强引用替换成弱引用,以此来解决问题。


总的来说,对于常见的内存泄漏场景,我们都可以通过画图来解决,本文为了介绍简便,使用了比较简单常见的例子,实际上,遇到复杂的内存泄漏,也可以通过画图的方式来解决。当然,熟练之后,省略画图的操作,也是可以的。


REFERENCE


wikipedia 内存泄漏


Excalidraw — Collaborative whiteboarding made easy


How LeakCanary works - LeakCanary


理解Java的强引用、软引用、弱引用和虚引用 - 掘金


作者:很好奇
来源:juejin.cn/post/7313242069099872306

0 个评论

要回复文章请先登录注册