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

内存泄漏简介

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

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

这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的 ActivityFragment 、fragment ViewViewModel 如果没有被回收,当前应用被判定为发生了内存泄漏。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。简单了解一下即可,不太清楚的话也不会影响接下来的问题解决。 同时,为了简单明了,文中还简化了一些相关但是不重要的引用链关系,比如 HandlerMessageQueue 的引用。

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

之前的 Activity 被释放,引用链 4 被切断了。我们可以很清晰的看到,由于 LeakHandlerMainActivity 的强引用(引用链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 表示的是 NoLeakHandlerWeakReference 的强引用,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

wikipedia 内存泄漏

Excalidraw --- Collaborative whiteboarding made easy

How LeakCanary works - LeakCanary

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

相关推荐
常利兵2 分钟前
Android内存泄漏:成因剖析与高效排查实战指南
android
·云扬·5 分钟前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
野生技术架构师7 分钟前
SQL语句性能优化分析及解决方案
android·sql·性能优化
doupoa1 小时前
内存指针是什么?为什么指针还要有偏移量?
android·c++
非凡ghost2 小时前
PowerDirector安卓版(威力导演安卓版)
android·windows·学习·软件需求
独行soc3 小时前
2026年渗透测试面试题总结-19(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
爱装代码的小瓶子4 小时前
【C++与Linux基础】进程间通讯方式:匿名管道
android·c++·后端
兴趣使然HX4 小时前
Android绘帧流程解析
android
JMchen1235 小时前
Android UDP编程:实现高效实时通信的全面指南
android·经验分享·网络协议·udp·kotlin
黄林晴6 小时前
Android 17 再曝猛料:通知栏和快捷设置终于分家了,这操作等了十年
android