关于android:为什么Handler会导致内存泄漏

38次阅读

共计 6634 个字符,预计需要花费 17 分钟才能阅读完成。

最近在思考对于内存泄露的问题,进而想到了对于咱们最常见和熟知的 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,如有侵权,请分割删除。

正文完
 0