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

相关推荐
拭心10 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王12 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡13 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道13 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库14 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道14 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe15 小时前
Android Hook - 动态加载so库
android
居居飒15 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He18 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗18 小时前
Android笔试面试题AI答之Android基础(1)
android