Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >为什么Handler会导致内存泄漏?

为什么Handler会导致内存泄漏?

原创
作者头像
没关系再继续努力
修改于 2021-11-22 06:28:03
修改于 2021-11-22 06:28:03
1.4K0
举报
文章被收录于专栏:Android面试Android面试

最近在思考关于内存泄露的问题,进而想到了关于我们最常见和熟知的Handler在Activity内导致的内存泄漏的问题,这个问题相信作为开发都是很熟悉的,但是这背后更多的细节和导致泄漏的不同的情况,可能很多人就没有那么了解和清楚了,因此这次和大家分享一下什么情况下会导致内存泄漏,以及内存泄漏背后的故事。

1.Handler在什么情况下会导致内存泄漏

Handler在使用过程中,什么情况会导致内存泄漏?如果大家搜索的话,一般都是会查到,Handler持有了Activity的引用,导致Activity不能正确被回收,因此导致的内存泄漏。那么这里就有问题了,什么情况下Handler持有了Activity的引用?什么时候Activity会不能被正确回收?

因此我们现在看两段段代码

代码1-1:

代码语言:txt
AI代码解释
复制
private fun setHandler() {  
        val handler \= object : Handler() {  
            override fun handleMessage(msg: Message) {  
                if (msg.what \== 1) {  
                    run()  
                }  
            }  
        }  
        handler.sendEmptyMessageDelayed(1, 10000)  
    }  
  
    private fun run() {  
        Log.e("Acclex", "run")  
    }

代码1-2:

代码语言:txt
AI代码解释
复制
private fun setHandler() {  
        val handler \= LeakHandler()  
      handler.sendEmptyMessageDelayed(1, 10000)  
}  
private fun run() {  
    Log.e("Acclex", "run")  
}  
  
inner class LeakHandler : Handler() {  
    override fun handleMessage(msg: Message) {  
        if (msg.what \== 1) {  
            run()  
        }  
    }  
}

相信Android的小伙伴应该都能看出来,上面两段代码都是会导致内存泄漏的,我们首先需要分析一下为什么会导致内存泄漏。以及藏在内存泄漏背后的事。

2.为什么会导致内存泄漏

上面的两段代码会导致内存泄漏,为什么会导致内存泄漏呢?这个问题也很好回答,因为匿名内部类和默认的内部类会持有外部类的引用。

Java中,匿名内部类和内部的非静态类在实例化的时候,默认会传入外部类的引用this进去,因此这两个handler会持有Activity的实例,当handler内有任务在执行的时候,我们关闭了Activity,这个时候回导致Activity不能正确被回收,就回导致内存泄漏。

从上面的代码中我们可以看到handler延时10秒发送了一个消息,如果在任务还未执行的时候,我们关闭Activity,这个时候Activity就回出现内存泄漏,LeakCanary也会捕获到内存泄漏的异常。但是如果我们等待任务执行完毕,再关闭Activity,是不会出现内存泄漏,LeakCanary也不会捕获到有什么异常。也就是说如果任务被执行完成了,那么Handler持有Activity引用将可以被正确的释放掉。

这里将会引申出一个新的问题,Handler内执行任务的是什么东西,Handler内对象引用的链条是怎么样的,最终持有的对象是什么?

这个问题我们稍后再来解答,我们现在先来看看别的情况下的Handler。

3.静态内部类的Handler

Android的小伙伴们应该都知道在解决Handler内存泄漏的时候一般都使用静态内部类和弱引用,这样一般都可以解决掉内存泄漏的问题,那么这里有一个变种,会不会导致内存泄漏呢?下面可以看一下下面的代码

代码1-3:

代码语言:txt
AI代码解释
复制
class UnLeakHandler() : Handler() {  
    lateinit var activity: MainActivity  
  
    constructor(activity: MainActivity) : this() {  
        this.activity \= activity  
    }  
}

代码1-4:

代码语言:txt
AI代码解释
复制
class UnLeakHandler(activity: MainActivity) : Handler() {  
  
}

如上代码,代码1-3内,我们传入了引用并且存储了这个变量,代码1-4内我们传入了引用,但是并没有存储这个变量,那么这两种情况下,那种情况下会导致内存泄漏呢?

答案是代码1,我们传入了引用并且将它作为一个变量存储起来了,这样的情况下它会导致内存泄漏。

那么这个问题该如何解答?要解答这个问题我们需要先理解一下Java运行时的程序计数器,虚拟机堆栈,本地方法栈,方法区,堆以及可作为GCRoot的对象。

Java运行时数据区
  • 程序计数器 程序计数器就是当前执行字节码的信号的一个指示器,记录的是当前线程执行字节码指令的地址。通常它会改变,然后实现代码的流程控制,顺序执行,循环等。
  • 虚拟机栈 虚拟机栈是Java方法运行过程中的一个内存模型。虚拟机栈会给没一个即将运行的方法创建一个栈帧的区域,这块区域存储了方法在运行时所需要的一些信息,主要包括:
  1. 局部变量表:包含方法内的非静态变量以及方法形参,基本类型的存储值,引用对象的指向对象的引用。
  2. 操作数栈:存储中间的运算结果,方法入参和返回结果。
  3. 运行时常量池引用:主要包含的是当前方法对运行时常量池的引用,方便类在加载时进行动态链接,根据引用符号转变为对方法或者变量的引用。
  4. 方法出口返回信息:方法执行完毕后,返回调用位置的地址。
  • 本地方法栈 类似于虚拟机栈,但是是由一些Cor汇编操作的一些方法信息,存放这些非Java语言编写的本地方法的栈区。
  • 堆是运行时数据最大的一块区域,里面包含了绝大部分的对象(实例数组等)都在里面存储。堆是跟随者JVM的启动而创建的,我们创建的对象基本都在堆上分配,并且我们不需要主动去释放内存,因为GC会自动帮我们进行管理和销毁。这里GC相关的一些知识我们后面再做讲解。
  • 方法区 主要存储类的元信息(类的名字,方法,字段信息),静态变量,常量等。方法区这块在JDK不同版本有不同的实现方法,存储的东西也有变化,感兴趣的话大家可以自行了解。
GCRoot对象

GCRoot就如同字面描述的,GC开始的根对象,将GCRoot对象作为起点,向下搜索,走过的路径就是一个引用链,如果一个对象到GCRoot没有任何引用链,那么GC将会把这个对象的内存进行回收。

那么GCRoot有哪几种类型呢?或者说哪些对象可以作为GCRoot的对象呢?

  • 虚拟机栈引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

好了,现在我们可以解答上面的问题了,为什么代码1-3会导致内存泄漏而代码1-4不会导致内存泄漏,如果使用代码1-3,构造函数传入了外部的Activiy,并且这个Handler类将这个引用存储到了类的内部,也就是说这个引用被Handler存储到了堆的区域内,那么直到它被释放位置,它将一直持有Activity的引用。

而在代码1-4内,构造函数本质也是一种函数,执行的时候,是以栈帧的形式执行的,函数的形参被存储在了栈帧上,构造函数执行完毕之后,这个栈帧将会弹出栈,传入的形参会被直接销毁,因此本质上代码1-4内创建的Handler并没有持有Activity的引用

4.Handler导致内存泄漏时的引用链

我们看完了上面的Handler在几种情况下的内存泄漏以及不会导致泄漏的问题,再回到我们开始的一个问题:Handler内执行任务的是什么东西,Handler内对象引用的链条是怎么样的,最终持有的对象是什么?

要解答这个问题,我们需要去分析一下Handler的源代码。

首先,Handler作为匿名内部类和非静态内部类创建的时候会持有外部Activity的引用,我们调用Handler的sendMessage方法发送消息,我们先从这个方法看一下。

代码语言:txt
AI代码解释
复制
public final boolean sendEmptyMessage(int what){  
        return sendEmptyMessageDelayed(what, 0);  
    }  
  
    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {  
        Message msg = Message.obtain();  
        msg.what = what;  
        return sendMessageDelayed(msg, delayMillis);  
    }

可以看到上面的方法,发送一条Empty的Message都调用的是延迟发送的Message方法,区别只是延时不同。在sendEmptyMessageDelayed方法内,构造了一个Message并且传入了sendMessageDelayed,我们再往下看,看一下sendMessageDelayed方法

代码语言:txt
AI代码解释
复制
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {  
        if (delayMillis < 0) {  
            delayMillis \= 0;  
        }  
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);  
    }  
  
    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {  
        MessageQueue queue \= mQueue;  
        if (queue \== null) {  
            RuntimeException e \= new RuntimeException(  
                    this + " sendMessageAtTime() called with no mQueue");  
            Log.w("Looper", e.getMessage(), e);  
            return false;  
        }  
        return enqueueMessage(queue, msg, uptimeMillis);  
    }

上面的代码我们可以看到,sendMessageAtTime方法内,构造了一个MessageQueue并且这个MessageQueue默认使用的就是该Handler内的MessageQueue,然后调用enqueueMessage去发送这个msg,参数就是这个queue和msg,我们在看一下这个enqueueMessage方法

代码语言:txt
AI代码解释
复制
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,  
        long uptimeMillis) {  
    msg.target = this;  
    msg.workSourceUid = ThreadLocalWorkSource.getUid();  
  
    if (mAsynchronous) {  
        msg.setAsynchronous(true);  
    }  
    return queue.enqueueMessage(msg, uptimeMillis);  
}

在enqueueMessage内,我们终于找到了引用Handler的地方了,构造的这个msg内的target引用的就是当前的Handler,那么这个将要被传递出去的message引用了当前的Handler,那么下面还有接着引用吗?答案是当然,在调用MessageQueue的enqueueMessage方法的时候,会将msg传入。完整代码较长,这边只帖一部分

代码语言:txt
AI代码解释
复制
Message p \= mMessages;  
boolean needWake;  
if (p \== null || when \== 0 || when < p.when) {  
    // New head, wake up the event queue if blocked.  
    msg.next \= p;  
    mMessages \= msg;  
    needWake \= mBlocked;  
} else {  
    // Inserted within the middle of the queue.  Usually we don't have to wake  
    // up the event queue unless there is a barrier at the head of the queue  
    // and the message is the earliest asynchronous message in the queue.  
    needWake \= mBlocked && p.target \== null && msg.isAsynchronous();  
    Message prev;  
    for (;;) {  
        prev \= p;  
        p \= p.next;  
        if (p \== null || when < p.when) {  
            break;  
        }  
        if (needWake && p.isAsynchronous()) {  
            needWake \= false;  
        }  
    }  
    msg.next \= p; // invariant: p == prev.next  
    prev.next \= msg;  
}

这是执行enqueueMessage的一部分代码,我们可以看到这边MessageQueue内构造了一个新的Message p,并且将这个对象复制给了 传递进来的msg.next,同时在当前MessageQueue的mMessages为空,也就是当前默认情况下没有消息传递的时候,将msg赋值给了mMessages,那么MessageQueue持有了要传递的Message对象。

这样我们就可以很清晰的看到一个完整的引用链了。

MessageQueue引用了Message,Message引用了Handler,Handler默认引用了外部类Activity的实例。我们也可以在LeakCanary上看到一样的引用链,并且它的GCRoot是一个native层的方法,这块就涉及到MessageQueue的事件发送的机制,以及和Looper以及Looper内的ThreadLocal的机制了,这就是另外一个话题了。

这里让我们再回到之前的一个概念GCRoot,还记得我们提到GCRoot的时候说到过,如果一个对象和GCRoot对象没有一个引用链,那么它将被回收。因此,这里就是冲突点了,Activity被我们主动关闭了,这个时候我们告诉了虚拟机Activity可以被回收了,但是从GCRoot开始向下搜索,发现其实Activity其实是有一条引用链的,GCRoot不能把它回收掉,但是Activity已经被关闭了,因此这个时候就触发了内存泄漏,应该被销毁和释放的内存并没有正确被释放。

5.解决Handler内存泄漏的方法

那么我们现在来总结一下如何解决Handler内存泄漏的方法。

  1. 静态类和弱引用,这个方法相信大家都知道,静态类不会持有外部引用,弱引用可以防止Handler持有Activity
  2. Activity销毁,生命周期结束的时候清空MessageQueue内的所有Message

其实这两种方法都是通过断开引用用,让GCRoot不会有引用链连接到Activity,从而让Activity正常回收。

6.总结思考扩展

其实Handler的内存泄漏是一个很常见,也是大家开发会使用和碰到的问题,但是它背后其实包含了很多细节和原理,是我们可以了解的,同时这个问题还可以引申出别的问题,这里可以提一下,大家之后可以思考一下,也欢迎大家写出它们背后的原理和大家分享。

  • 我们常用的View.setOnClickListener很多时候也创建了匿名内部类或者是直接传入了Activity,为什么这种情况下的Activity或者Fragment没有泄露。
  • 我们在使用ViewModel以及LiveData的时候,构造这些对象,以及观察对应数据的时候,如果Activity或者Fragment关闭了,为什么不会导致内存泄漏。
  • 我们开发的时候,自己编码或者使用一些第三方库,例如RxJava的时候,如何尽量避免内存泄漏。

其实内存泄漏在不管什么语言,什么平台上,都是有可能发生的,而我们需要自己去主动关注这个方面,在编写代码的时候尽量规避掉一些可能会导致内存泄漏的代码。

相关视频推荐:

【Android handle面试】Handler中的Message如何创建?

【Android handle面试】MessageQueue如何保持各线程添加消息的线程安全?

本文转自 https://juejin.cn/post/6844904083795476487,如有侵权,请联系删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Handler、Message、Looper、MessageQueue
handler消息机制原理:本质就是在线程之间实现一个消息队列(MessageQueue)。 生产者Handler在异步线程通过sendMessageDelayed() 将消息添加至MessageQueue, 消费者Looper通过loop()中死循环将MessageQueue中的msg取出后发送给产生此msg的handler的handleMessage() 在主线程进行处理;
用户9854323
2022/06/25
3580
Handler、Message、Looper、MessageQueue
Handler面试必问八大题:如何深挖原理进大厂?1万+字带你详细剖析!
Handler一直是面试过程中的常客,我们今天来看看围绕Handler究竟能玩出那些花儿来。
Android技术干货分享
2021/08/27
1.5K0
Handler面试必问八大题:如何深挖原理进大厂?1万+字带你详细剖析!
Handler源码分析
Handler 引言 Handler是为了解决非UI线程中UI更新的问题,这里会产生一个疑问。为啥要在UI线程中更新,一般都知道会产生卡顿问题。 基本概念 上张官方关系类图,压压惊: 可以看到
用户1148881
2018/04/11
6730
Handler源码分析
一篇文章扒掉“桥梁Handler”的底裤
Android跨进程要掌握的是Binder, 而同一进程中最重要的应该就是Handler 消息通信机制了。我这么说,大家不知道是否认同,如果认同,还希望能给一个关注哈。
BlueSocks
2022/03/21
2320
一篇文章扒掉“桥梁Handler”的底裤
面试问关于Handler的这些问题你知道吗?
Q :在线程中可以直接调用 Handler 无参的构造方法吗?在主线程和子线程中有没有区别? A:在主线程中可以;在子线程中会抛出RuntimeException, 需要先调用 Looper.prepare()。主线程在启动的时候已经在调用过Looper.prepare()。
103style
2022/12/19
2930
深入Handler、Looper、MessageQueue
Handler、Looper、MessageQueue基本了解可以看下这篇文章 Android源码解读-Handler、Loop、MessageQueue
笔头
2022/01/24
3810
面试官带你学Android——面试中Handler 这些必备知识点你都知道吗?
在Android面试中,关于 Handler 的问题是必备的,但是这些关于 Handler 的知识点你都知道吗?
Android技术干货分享
2020/10/12
7190
面试官带你学Android——面试中Handler 这些必备知识点你都知道吗?
面试Handler都没答上来,你真的了解Handler吗?Handler全面解析来了!
用法很简单,定义一个handler,重写handleMessage方法处理消息,用send系列方法发送消息。 但是主线程和新建线程用法却有点不一样!其实新线程里面的用法才是表达出完整流程的。
Android技术干货分享
2020/11/27
1.2K0
面试Handler都没答上来,你真的了解Handler吗?Handler全面解析来了!
面试官还问Handler?那我要给你讲个故事
Handler的相关博客太多了,随便一搜都一大把,但是基本都是上来就贴源码,讲姿势,短时间不太好弄明白整体的关系,和流程.
没关系再继续努力
2021/12/25
4530
你真的了解Handler吗
用法很简单,定义一个handler,重写handleMessage方法处理消息,用send系列方法发送消息。 但是主线程和新建线程用法却有点不一样!其实新线程里面的用法才是表达出完整流程的。
码上积木
2020/12/11
7100
你真的了解Handler吗
Handler:Android 消息机制,我有必要再讲一次!
我们在日常开发中,总是不可避免的会用到 Handler,虽说 Handler 机制并不等同于 Android 的消息机制,但 Handler 的消息机制在 Android 开发中早已谙熟于心,非常重要!
Android架构
2019/07/24
4100
「细品源码」 Android 系统的血液:Handler
作为 Android 开发者,相信对于 Handler 的使用早已烂熟于心。Handler 对于 Android 非常重要,可以说,没有它,Android App 就是一堆“破铜烂铁”,它就像 Android 的血液,穿梭在 App 的各个角落,输送养分。
开发的猫
2020/06/19
1K0
「细品源码」 Android 系统的血液:Handler
Android点将台:烽火狼烟[-Handler-]
张风捷特烈
2024/02/11
1820
Android点将台:烽火狼烟[-Handler-]
从 Android 开发到读懂源码 第07期:Message 机制源码解析
核心类就是 ThreadLocal ,它提供线程局部变量,每个线程都有自己独立的一份变量,通常是类中的 private static 字段,它们希望将状态与某一个线程相关联,在多线程编程中常用,比如 Android 的绘制同步机制 Choreographer 中也有使用。
数据库交流
2022/04/25
3950
从 Android 开发到读懂源码 第07期:Message 机制源码解析
《Android面试题思考与解答》2021年3月刊
要声明的一点是:面试题的目的不是为了让大家背题,而是从不同维度帮助大家复习,取长补短。
码上积木
2021/04/16
1.5K0
Android程序员详解:Handler机制
Handler在我们日常开发中会经常用到,它主要用于处理异步消息,当发出一个消息之后,首先进入到一个消息队列,发送消息的函数即可返回,而另外一个部分在消息队列中逐一取出,然后对消息进行处理。
Android架构
2019/07/08
7180
Android程序员详解:Handler机制
Android全面解析之由浅及深Handler消息机制
关于Handler的博客可谓是俯拾皆是,而这也是一个老生常谈的话题,可见的他非常基础,也非常重要。但很多的博客,却很少有从入门开始介绍,这在我一开始学习的时候就直接给我讲Looper讲阻塞,非常难以理解。同时,也很少有系统地讲解关于Handler的一切,知识比较零散。我希望写一篇从入门到深入,系统地全面地讲解Handler的文章,帮助大家认识Handler。
huofo
2022/03/18
8420
Android全面解析之由浅及深Handler消息机制
面试必考体系庞大的Handler你真的都了解吗?Handler二十七问带你打破砂锅问到底!
既然它如此重要,不知对面的你了解它多深呢?今天就和大家一起打破砂锅问到底,看看Handler这口砂锅的底到底在哪里。
Android技术干货分享
2021/03/24
5710
面试必考体系庞大的Handler你真的都了解吗?Handler二十七问带你打破砂锅问到底!
Android Handler机制7之消息发送
光看上面这些API你可能会觉得handler能法两种消息,一种是Runnable对象,一种是message对象,这是直观的理解,但其实post发出的Runnable对象最后都封装成message对象了。
隔壁老李头
2018/08/30
1.4K0
Android Handler机制7之消息发送
Android 一起来看看面试必问的消息机制
Android 消息机制的主要是指的是 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 Looper 的工作过程,这三者实际上是一个整体,只不过我们在开发过程中比较多地接触 Handler 而已。
developerHaoz
2018/08/20
3430
推荐阅读
相关推荐
Handler、Message、Looper、MessageQueue
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档