最近在思考对于内存泄露的问题,进而想到了对于咱们最常见和熟知的Handler在Activity内导致的内存透露的问题,这个问题置信作为开发都是很相熟的,然而这背地更多的细节和导致透露的不同的状况,可能很多人就没有那么理解和分明了,因而这次和大家分享一下什么状况下会导致内存透露,以及内存透露背地的故事。

1.Handler在什么状况下会导致内存透露

Handler在应用过程中,什么状况会导致内存透露?如果大家搜寻的话,个别都是会查到,Handler持有了Activity的援用,导致Activity不能正确被回收,因而导致的内存透露。那么这里就有问题了,什么状况下Handler持有了Activity的援用?什么时候Activity会不能被正确回收?

因而咱们当初看两段段代码

代码1-1:

    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:

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:

class UnLeakHandler() : Handler() {    lateinit var activity: MainActivity    constructor(activity: MainActivity) : this() {        this.activity = activity    }}

代码1-4:

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办法发送音讯,咱们先从这个办法看一下。

    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办法

    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办法

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传入。残缺代码较长,这边只帖一部分

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如何放弃各线程增加音讯的线程平安?
Android(安卓)开发零根底从入门到精通教程:Studio装置/UI/Fragment/四大组件/风行框架/我的项目公布与治理/我的项目实战

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