文章简介
Android Handler的泄漏算是很有名了,Handler稍有不慎就会造成泄漏。上网一搜就能搜到一大堆解释的文章。但是,大部分其实都在翻译或者解释这篇著名的外文:
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
这篇文章介绍了Handler发送的message以postDelayed的方式驻留在MessageQueue而引起内存泄漏的情况。
配合Handler-Looper-Message机制的理解,看完这篇文章,有一种恍然大悟的激动。
但是!
我们在写handler回发message的时候其实用postDelay的情况也不是占绝大部分,那是不是就不用处理泄漏的情况了呢?
我想啊想,于是想到 线程处理的延时 会不会造成泄漏呢,个人觉得是会的,但是希望求证一下,于是懒得不能自理的我开始在某度和G**gle上搜答案,搜了半天,可能因为上面那篇外文太酷炫,搜出的文章几乎全是讲的是外文中提及的情况。而且在这篇文章中
https://juejin.im/entry/58da161361ff4b0060716f02
作者提及handler泄漏的时候提及 * “只有postDelayed的时候才会有泄露问题,因为delayed的时候activity的引用还保持着,所以只要delayed完了就能回收了,大多数情况下根本不必用加static。” *
这一看我就怂了,因为自己感觉开匿名线程的情况还是挺多,如果线程泄漏的话handler的泄漏还是要处理一下的,可能作者并没有线程不会泄漏的意思,但我这云里雾里的,实在没办法,只好爬起来自己测试一番。于是,这篇文章诞生了。
文章会首先介绍外文提及的泄漏原理及测试,已经熟烂的兄弟姐妹可以直接跳过,后面会介绍线程与handler的配合导致泄漏的原理与测试结果, 大佬们肯定不用测试也心里有数,因此对java回收以及handler机制已经理解透彻的大佬默默地点一下网页右上角的叉叉就好了。
言归正传,本文使用的泄漏测试用的正是你们熟悉的LeakCanary 1.4,那么,现在开始。
Message驻留MessageQueue的泄漏情况
这种情况正是文章开头提到的那篇外文中提及的情况。来看一段代码
public class MainActivity extends AppCompatActivity {
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
private Thread leakThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
leakThread = new Thread(new LeakRunnable(handler));
leakThread.start();
Button button = (Button) findViewById(R.id.btn_start);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.StartSecondActivity(MainActivity.this);
finish();
}
});
}
}
这段代码相当简单,只有三个点
- 有一个内部匿名Handler类。
- 有一个私有线程成员,leakThread,线程的runnable来自Runnable实现类 LeakRunnable(代码后面贴出,也很简单),并且这个Runnable注入了handler,内部持有handler这个引用。
- 有一个button,点击会跳转到别的activity并finish(),这样的话,在正常情况下garbage collector就会在合适的时候回收MainActivity对象。
好,代码看完了,首先明确一点: java的内部类会默认持有外部类的对象引用。在这段代码的表现就是handler会持有MainActivity这个对象的引用。
然后要知道这段代码有两条关键的引用链,
第一条,从这段代码就能看出来的:
mainActivity -(1.1)-> leakThread -(1.2)-> handler -(1.3)-> mainActivity
第二条,从Handler->Looper->MessageQueue机制看出来的:
sMainLooper-(2.1)->mMessageQueue-(2.2)->mMessage-(2.3)->handler-(2.4)->mainActivity
解释一下第二条链是怎么出现的:
主线程拥有一个Looper叫sMainLooper,这个Looper是静态变量,与程序共存亡,而Looper中持有一个MessageQueue的对象,可以看Looper的源码(只贴出了一小部分),里面有个mQueue的成员变量
public final class Looper {
/*
* API Implementation Note:
*
* This class contains the code required to set up and manage an event loop
* based on MessageQueue. APIs that affect the state of the queue should be
* defined on MessageQueue or Handler rather than on Looper itself. For example,
* idle handlers and sync barriers are defined on the queue whereas preparing the
* thread, looping, and quitting are defined on the looper.
*/
private static final String TAG = "Looper";
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal sThreadLocal = new ThreadLocal();
private static Looper sMainLooper; // guarded by Looper.class
final MessageQueue mQueue;
}
MessageQueue中持有message对象,同样,源码中有个mMessage的对象
public final class MessageQueue {
private static final String TAG = "MessageQueue";
Message mMessages;
}
Message中持有Handler对象, 在handler发送消息时会把持有的handler引用指向发送自己的handler,在源码中这个对象名叫target, 代码就不贴出来啦。
因此出现了上面所说的引用链。
当LeakRunnable的实现是如下图所示的时候,handler发送一个10分钟延迟的消息,造成的就是经典的message驻留在messageQueue引起泄漏的情况。
public class LeakRunnable implements Runnable {
private Handler handler;
private Message msg;
public LeakRunnable(Handler handler){
this.handler = handler;
msg = new Message();
}
@Override
public void run() {
MessageQueue_Message_Leak();
}
public void MessageQueue_Message_Leak(){
msg.what = 0;
handler.sendMessageDelayed(msg,1000 * 60 * 10);
}
}
我们可以从代码很容易分析到,当activity需要被回收时,由于message需要在MessageQueue中驻留10分钟,此时第二条引用链无法断开,使得本应该被回收的mainActivity被强引用持有而无法回收。分析到这里,我们运行程序点击start,等几秒就会收到LeakCanary的推送了,看图!
结果正如分析所提到的一样,引用链的(2.2),(2.3),(2.4)节点都出现在了推送上。
这种泄漏情况就分析到这就结束了,还不懂的可以看看链接的外文,文章写得相当清楚,下面进入下一章,分析一个使用handler更新ui的线程在处理耗时操作造成的泄漏情况。
带有耗时操作的线程通过handler更新UI造成泄漏的情况
首先把上一章的引用链再贴一遍,这一章要用到
mainActivity -(1.1)-> leakThread -(1.2)-> handler -(1.3)-> mainActivity
sMainLooper-(2.1)->mMessageQueue-(2.2)->mMessage-(2.3)->handler-(2.4)->mainActivity
测试主界面依然跟上一章一样,不同的是LeakRunnable的run逻辑。
public class LeakRunnable implements Runnable {
private Handler handler;
private Message msg;
public LeakRunnable(Handler handler){
this.handler = handler;
msg = new Message();
}
@Override
public void run() {
Thread_Handler_Leak();
}
public void Thread_Handler_Leak(){
while(true){
try {
Thread.sleep(1000 * 10 * 60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这次runnable里面甚至没有使用handler发送消息,仅仅是把主线程的handler注入进来,并且run方法模拟了一个耗时操作。由于没有发送消息,这下跟什么Message,MessageQueue没关系了,也就是(2.2),(2.3)节点断开了。那不会泄漏了吧?
答案当然是否定的。为什么?因为我还没提到过第一条引用链呀。
当handler不发送message的时候第一条引用链还是存在的,试想,如果耗时操作存在,节点(1.2)(1.3)是会长时间存在的。
但聪明的你一定会问:那(1.1)呢?!
没错,(1.1)的存在表明了mainActivity跟leakThread对象的关系有点像循环引用,只是多了个handler作为中间者来桥接,而handler的生命周期在这种情况下完全是依赖于thread或者mainAcitivity的,因此handler对分析泄漏过程不起关键作用。按照现代java gc来说,什么循环引用都是渣渣,我们有可达性算法,标记清除法,不会泄漏!
(关于java垃圾回收这方面不熟悉的可以看看这个
http://www.cnblogs.com/sunniest/p/4575144.html)
那么,真的不会泄漏吗?
点击一下界面的start,现在看看LeakCanary的推送:
好的,泄漏了。泄漏的正是第一条引用链的整条链。
为什么?因为可达性分析算法依赖定义的GC Root对象,参考java文档
https://www.yourkit.com/docs/java/help/gc_roots.jsp
可知道live Thread是被jvm识别为GC Root的,因此只要leakThread活着,即使activity生命周期已经结束,可达性分析算法会觉得第一条链中整条链的对象均不应该被回收,泄漏就会发生。
这种泄漏应该引起我们注意,因为我们经常都会传入一个handler引用到子线程来通知activity更新ui,而子线程往往都有耗时任务要处理,因此我们写代码的时候很容易就在不知不觉中操作到了内存泄漏的handler。
至于怎么解决?断开引用链呗。怎么断?方法多的是
- 比如使用弱引用来引用传进来的handler,这样(1.2)节点就会断开(但这样做需要注意在通知ui更新时对handler的引用判空,不然你的老朋友NullPointException一定会来光顾的,为什么?都有耐心看到这来,你就结合上面说的思考一下呗)。
public class LeakRunnable implements Runnable {
private WeakReference handler;
private Message msg;
public LeakRunnable(Handler handler){
this.handler = new WeakReference(handler);
msg = new Message();
}
@Override
public void run() {
Thread_Handler_Leak();
}
public void Thread_Handler_Leak(){
while(true){
try {
Thread.sleep(1000 * 60 * 10 );
if(handler.get() != null) {
handler.get().sendEmptyMessage(0);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Handler定义为静态内部类,这样做handler就不会持有mainActivity的引用。但这样的话就不方便我们更新ui。因此可以同样地传一个mainActivity的弱引用进去。
在mainActivity destroy的时候停止线程的工作并回收线程资源。
解决方法我只提供了思路,就不细讲了,各位老铁那么聪明,思考一下肯定就实现了。到这里测试与分析就结束啦。