内存走漏简介

关于内存走漏的界说,想必咱们已经烂熟于心了,简单一点的说,便是程序占用了不再运用的内存空间

那在 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() 办法之后的引证链图示:

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

简单阐明一下引证链 0 的位置,这儿为了简化,直接运用 GCRoot 代替了,实际上存在这样的引证联系:GCRoot → ActivityThread → Handler → MessageQueue → Message。简单了解一下即可,不太清楚的话也不会影响接下来的问题处理。 一起,为了简单明晰,文中还简化了一些相关可是不重要的引证链联系,比方 HandlerMessageQueue 的引证。

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

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

之前的 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 就能够正常收回了。

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

方案二:

运用弱引证 + 静态内部类的办法,咱们同样也能够处理这个内存走漏问题,想必咱们已经十分熟悉了。

这儿再简单阐明一下弱引证 + 静态内部类的原理: 弱引证的收回机制:在垃圾收回器线程扫描它所统辖的内存区域的过程中,一旦发现了只被弱引证引证的目标,不管当时内存空间足够与否,都会收回它的内存。 静态内部类:静态内部类不会持有外部类的引证,也就不会有 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,运用虚线表明),是能够正常被收回的。

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

另一个简单的比方

再来一个静态类持有 Activity 的比方,如下是要害代码:

object LeakStaticObject {
    val activityCollection: MutableList<Activity> = mutableListOf()
}
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // other code
        activityCollection.add(this)
    }
}  

正常运行的状况下,存在如下的引证联系:

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

在旋转屏幕之后,会产生内存走漏,因为之前的 MainActivity 还被 GCRoot 节点(LeakStaticObject)引证着。那要怎么处理呢?信任咱们已经比较清楚了,要么堵截引证链 0,要么将引证链 0 替换成一个弱引证。因为比较简单,这儿就不再独自画图阐明晰。

总结

本文介绍了一种运用画图来处理常见内存走漏的办法,会比直接检查引证链愈加清晰具体。一起,其相比于一些概括常见内存走漏的办法,会愈加的通用,很大程度上摆脱了对内存走漏场景的强行回忆。

经过画图,找到引证途径之后,在引证链的某个节点上进行操作,堵截强引证或许将强引证替换成弱引证,以此来处理问题。

总的来说,对于常见的内存走漏场景,咱们都能够经过画图来处理,本文为了介绍简便,运用了比较简单常见的比方,实际上,遇到杂乱的内存走漏,也能够经过画图的办法来处理。当然,熟练之后,省略画图的操作,也是能够的。

REFERENCE

wikipedia 内存走漏

Excalidraw — Collaborative whiteboarding made easy

How LeakCanary works – LeakCanary

了解Java的强引证、软引证、弱引证和虚引证 –