目录
前言
一直以来,无论是在工作还是面试中,关于内存泄漏的话题,肯定是绕不开的。其中最典型的就是handler的内存泄漏问题,今天就带着大家一起探究一下handler内存泄漏的真正原因和场景。
一、handler的几种创建方式
先上图:
相信大家在平时开发过程中定义handler时,最常见就是以上这三种了。
第一种:直接使用匿名内部类
第二种:内部类方式
第三种:静态内部类方式
那么这三种方式肯定是有区别的,区别在哪里?
查看一下编译后的字节码:
可以看到SecondActivity被分成了四部分,很容易看出SecondActivity$1、SecondActivity$handler1、SecondActivity$handler2分别对应了以上三种创建方式。
先看SecondActivity$1:
原来匿名内部类方式,构造函数中传入了一个外部类对象的引用,且被handler持有了,这里指的就是activity被持有了。
再看SecondActivity$handler1:
和SecondActivity$1一样,activity也被持有了。
最后看看SecondActivity$handler2:
可以看到静态内部类方式,不会持有activity的引用。
ps:这里其实跟Android没啥关系,纯粹的java基础。。。
二、何时产生内存泄漏?
有了上面的分析,我可以初步得出结论:使用第三种方式即静态内部类的方式是最“保险”的。
但是有一点要说明,并不是说第三种就一定不会产生内存泄漏,其他两种方式就一定会产生内存泄漏,实际还是要看后续的用法。
ok,我们继续往下分析如何产生的。
先在原有的代码上补充一些逻辑:
三个handler各自向主线程的消息队里投递了消息。
跟一下handler#post的源码:
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
/
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);
}
///
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这个方法中,"msg.target = this",原来msg的target持有了当前的handler。
ps:很多博文都喜欢用postdelay举例,其实个人觉得post也可以解释。因为post出去的消息,不一定会立即被执行,尤其是在looper很忙的时候。
假设此时消息都没有被执行,而activity现在被finish掉了,此时三种方式对象之间的持有关系:
1)匿名内部类:handler持有activity msg持有handler
2)内部类:handler持有activity msg持有handler
3)静态内部类:handler不持有activity msg持有handler
根据gc原理(可达性算法),可以指中1和2中ativity都不能被回收,3可以被回收。
那么仅仅使用静态内部类,就可以完全避免了吗?答案:不是的
接下来我们修改一下代码:
一目了然,还是上面的情形,还是会造成内存泄漏。
怎么改呢?如下:
比较简单,就是使用弱引用。。
三、再谈handler内存泄漏
不知道大家有没有发现,上面所述设定的场景,其实只会造成activity暂时无法回收,当时handler的任务执行完时,当jvm再次gc,activity还是能被回收的,因为,任务执行完后,对应的msg会被回收,target会被置空,handler被解除持有,可以被回收,随即activity当然也可以被回收了。
所以严格来讲,还是不会发生内存泄露。
那么什么场景下才会发生呢?
我们再修改一下代码:
类似搞了不断执行的任务,可能这里的写法不严谨,只是为了表现一种不断执行的状态,实际项目中,我们在使用handler时,很少让全局的handler执行执行一次任务,太浪费了。。。如果真的只需要执行一次任务,那直接就是写成了new Handler().post.....
归回正题,如果是这样的场景,那就会有不断的msg持有handler对象,handler又持有了activity,这样的结果就是activity是真的不能回收了,即发生内存泄漏。
那么此种情况下怎么解决呢?
在onDestroy时使用removeCallbacksAndMessages移除所有未执行的消息,即可停止handler。
另外,我以前一直有这么个疑惑:根据removeCallbacksAndMessages的源码得知,该方法是移除属于该handler的所有等待执行的消息,那么正在执行消息无法移除,那岂不是会发生停止不了的情况吗? --------------现在我明白了,其实post和ondestroy同在主线程,是同步的,一旦ondestroy执行,dodo肯定不会执行,也就是说当前正在执行的任务不是handler2投递的消息而是执行的是生命周期相关handler投递的消息(主线程的looper只有一个,handler却有多个),此时removeCallbacksAndMessgage清空了所有handler2的消息,终止了handler2。
到此,分析完结,,睡觉咯