最近在思考关于内存泄露的问题,进而想到了关于我们最常见和熟知的Handler在Activity内导致的内存泄漏的问题,这个问题相信作为开发都是很熟悉的,但是这背后更多的细节和导致泄漏的不同的情况,可能很多人就没有那么了解和清楚了,因此这次和大家分享一下什么情况下会导致内存泄漏,以及内存泄漏背后的故事。
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的小伙伴应该都能看出来,上面两段代码都是会导致内存泄漏的,我们首先需要分析一下为什么会导致内存泄漏。以及藏在内存泄漏背后的事。
上面的两段代码会导致内存泄漏,为什么会导致内存泄漏呢?这个问题也很好回答,因为匿名内部类和默认的内部类会持有外部类的引用。
在Java中,匿名内部类和内部的非静态类在实例化的时候,默认会传入外部类的引用this进去,因此这两个handler会持有Activity的实例,当handler内有任务在执行的时候,我们关闭了Activity,这个时候回导致Activity不能正确被回收,就回导致内存泄漏。
从上面的代码中我们可以看到handler延时10秒发送了一个消息,如果在任务还未执行的时候,我们关闭Activity,这个时候Activity就回出现内存泄漏,LeakCanary也会捕获到内存泄漏的异常。但是如果我们等待任务执行完毕,再关闭Activity,是不会出现内存泄漏,LeakCanary也不会捕获到有什么异常。也就是说如果任务被执行完成了,那么Handler持有Activity引用将可以被正确的释放掉。
这里将会引申出一个新的问题,Handler内执行任务的是什么东西,Handler内对象引用的链条是怎么样的,最终持有的对象是什么?
这个问题我们稍后再来解答,我们现在先来看看别的情况下的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的对象。
GCRoot就如同字面描述的,GC开始的根对象,将GCRoot对象作为起点,向下搜索,走过的路径就是一个引用链,如果一个对象到GCRoot没有任何引用链,那么GC将会把这个对象的内存进行回收。
那么GCRoot有哪几种类型呢?或者说哪些对象可以作为GCRoot的对象呢?
好了,现在我们可以解答上面的问题了,为什么代码1-3会导致内存泄漏而代码1-4不会导致内存泄漏,如果使用代码1-3,构造函数传入了外部的Activiy,并且这个Handler类将这个引用存储到了类的内部,也就是说这个引用被Handler存储到了堆的区域内,那么直到它被释放位置,它将一直持有Activity的引用。
而在代码1-4内,构造函数本质也是一种函数,执行的时候,是以栈帧的形式执行的,函数的形参被存储在了栈帧上,构造函数执行完毕之后,这个栈帧将会弹出栈,传入的形参会被直接销毁,因此本质上代码1-4内创建的Handler并没有持有Activity的引用
我们看完了上面的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已经被关闭了,因此这个时候就触发了内存泄漏,应该被销毁和释放的内存并没有正确被释放。
那么我们现在来总结一下如何解决Handler内存泄漏的方法。
其实这两种方法都是通过断开引用用,让GCRoot不会有引用链连接到Activity,从而让Activity正常回收。
其实Handler的内存泄漏是一个很常见,也是大家开发会使用和碰到的问题,但是它背后其实包含了很多细节和原理,是我们可以了解的,同时这个问题还可以引申出别的问题,这里可以提一下,大家之后可以思考一下,也欢迎大家写出它们背后的原理和大家分享。
其实内存泄漏在不管什么语言,什么平台上,都是有可能发生的,而我们需要自己去主动关注这个方面,在编写代码的时候尽量规避掉一些可能会导致内存泄漏的代码。
相关视频推荐:
【Android handle面试】Handler中的Message如何创建?
【Android handle面试】MessageQueue如何保持各线程添加消息的线程安全?
本文转自 https://juejin.cn/post/6844904083795476487,如有侵权,请联系删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有