zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

2023-03-15 22:04:18 时间

最近在思考关于内存泄露的问题,进而想到了关于我们最常见和熟知的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如何保持各线程添加消息的线程安全?

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