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

相关推荐
深海呐1 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang1 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼1 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss2 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19435 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男6 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽7 小时前
Android 源码集成可卸载 APP
android
码农明明7 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风8 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教9 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python