【内存泄漏】图解 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的强引用、软引用、弱引用和虚引用 - 掘金

相关推荐
大白要努力!17 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟1 小时前
Android音频采集
android·音视频
小白也想学C2 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程2 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记