介绍
最近在恶补Handler的知识,其中就涉及到了Handler引起的内存泄露问题,网络上有很多的分析文章。我就按照这些文章的思路,写代码验证,主要是验证和记录。
使用的内存检测工具是:LeakCanary 中文使用说明
英文原文:
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
翻译得比较好的中文版:
技术小黑屋:http://droidyue.com/blog/2014/12/28/in-android-handler-classes-should-be-static-or-leaks-might-occur/
简书博客:http://www.jianshu.com/p/cb9b4b71a820
问题代码
首先根据原文的思想写出会引起内存泄露的代码。这里就不使用postDelayed()方法,发送Runnable匿名类(非静态匿名类)为了简化问题。使用的是sendEmptyMessageDelayed()
发送延时消息。
非静态匿名类:同样持有对其外部类的引用,也会导致泄漏。
public class LeakActivity extends BaseActivity {
//省略其他代码
private Handler mLeakHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Logger.d(msg.toString());
}
};
@Override
protected void onResume() {
super.onResume();
mLeakHandler.sendEmptyMessageDelayed(0x1,10000);
finish();
}
//省略其他代码
}
代码逻辑大概就这么多,在其他地方跳转到LeakActivity
过来就可以。
然后喜闻乐见的内存就发生了,请看下图。
内存泄露
看图分析:
- 根据图片可以分析最终的泄露发生在LeakActivity实例中
- 其他类是Handler四件套,Looper+MessageQueue+Message+Handler,它们之间互相配合实现Android内部的多线程通信,当然上面的代码只在UI线程运行。
- 在发送的延迟空消息(EmptyMessageDelayed)内部持有对handler的引用,而handler又持有对其外部类(即LeakActivity实例)的潜在引用。
- 这个消息层层传递被static修饰的静态MainLooper持有,静态变量的生命周期与应用程序一致。
- 这条引用关系会一直保持直到消息得到处理,从而,这阻止了LeakActivity被垃圾回收器回收GC,同时造成应用程序的内存泄漏。
类的生命周期
再看张图:
这是LeakActivity实例和内部的Handler实例的打印结果。
- 因为把finish方法写在onResume()方法中,生命周期快速的走了一遍。
- 24:07的时候调用了onDestroy方法,而在24:20延迟10几秒之后,Handler实例才调用handleMessage处理了消息。
- 最后在下一次的垃圾回收周期LeakActivity实例最终还是被回收了,JVM保证在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。我这里是重写finalize方法打印出回调结果。
- 如果你在消息处理之后,使用Android Studio的Initate GC手动GC的话。Log消息结果也是一样的,还可以看到内存变化。
不会内存泄露的Handler代码
使用WeakReference弱引用持有Activity实例,静态内部类MyHandler处理消息。
相关技术点
静态内部类:不会持有对外部类的引用
弱引用:弱引用的对象拥有短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
代码如下:
private static class MyHandler extends Handler {
private WeakReference<Activity> reference;
public MyHandler(Activity activity) {
reference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
LeakActivity activity = (LeakActivity) reference.get();
if (activity != null) {
Logger.d("activity != null"+activity.toString());
} else {
Logger.d("activity = null");
}
}
}
private final Handler mHandler = new MyHandler(this);
然后mHandler
发送延迟10000毫秒的延迟空消息
mHandler.sendEmptyMessageDelayed(0,10000);
分析
首先自然是没有内存泄露了,看日志打印结果
- LeakActivity实例在调用onDestroy方法。
- 系统在之后5秒左右回收了实例内存
- 当再去处理消息的时候发现弱引用持有的LeakActivity实例为空,打印结果
等等感觉还有缺什么
修改延迟时间
哪如果修改延迟消息的发送时间会发生什么。
修改代码如下:2秒的延迟,这也是平常开发比较普遍的延迟时间。
mHandler.sendEmptyMessageDelayed(0,2000);
再看日志打印:
- 消息处理时LeakActivity实例竟然不为空。
- 进入了处理LeakActivity实例的代码块打印出了消息。
- 5秒之后才回收了实例内存。刚好就印证了上面的话:
由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
当我们在Activity实例已经调用onDestroy方法不可见的情况下,还操作了实例修改视图做操作肯定是没有意义的,但是这样的操作也不会报出异常,只是感觉不太好。
最后的代码
通过上文的分析,WeakReference弱引用可以做到不会内存泄漏。但是实际上Handler可以直接调用方法清除掉所有的回调消息。
所以最后其实在使用Handler的时候,在生命周期的中加上一行代码,就可以了。
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
再看日志打印结果:
完美的结果!
总结
- 这篇博文主要思路来自与英文原文,并没有止步于理解还动手写代码。
- 在写代码运行程序分析之后发现问题,分析弱引用的实际效果,最后的代码部分提供最终解决方案。
- 本文仅供大家参考,我使用的Flyme系统,GC系统回收周期在5秒左右,根据不同Rom应该会有差别。