Android开发-Handler引起的内存泄漏-实验、分析、总结。

介绍

最近在恶补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();
    }
    //省略其他代码
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

代码逻辑大概就这么多,在其他地方跳转到LeakActivity过来就可以。 
然后喜闻乐见的内存就发生了,请看下图。 
Handler导致的内存泄露

内存泄露

看图分析:

  • 根据图片可以分析最终的泄露发生在LeakActivity实例中
  • 其他类是Handler四件套,Looper+MessageQueue+Message+Handler,它们之间互相配合实现Android内部的多线程通信,当然上面的代码只在UI线程运行。
  • 在发送的延迟空消息(EmptyMessageDelayed)内部持有对handler的引用,而handler又持有对其外部类(即LeakActivity实例)的潜在引用。
  • 这个消息层层传递被static修饰的静态MainLooper持有,静态变量的生命周期与应用程序一致。
  • 这条引用关系会一直保持直到消息得到处理,从而,这阻止了LeakActivity被垃圾回收器回收GC,同时造成应用程序的内存泄漏。

类的生命周期

再看张图: 
Log打印结果
这是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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

然后mHandler 发送延迟10000毫秒的延迟空消息

  mHandler.sendEmptyMessageDelayed(0,10000);
  • 1

分析

首先自然是没有内存泄露了,看日志打印结果 
弱引用实例被回收

  • LeakActivity实例在调用onDestroy方法。
  • 系统在之后5秒左右回收了实例内存
  • 当再去处理消息的时候发现弱引用持有的LeakActivity实例为空,打印结果

等等感觉还有缺什么

修改延迟时间

哪如果修改延迟消息的发送时间会发生什么。 
修改代码如下:2秒的延迟,这也是平常开发比较普遍的延迟时间。

 mHandler.sendEmptyMessageDelayed(0,2000);
  • 1

再看日志打印: 
2秒延迟消息

  • 消息处理时LeakActivity实例竟然不为空。
  • 进入了处理LeakActivity实例的代码块打印出了消息。
  • 5秒之后才回收了实例内存。刚好就印证了上面的话:

由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

当我们在Activity实例已经调用onDestroy方法不可见的情况下,还操作了实例修改视图做操作肯定是没有意义的,但是这样的操作也不会报出异常,只是感觉不太好。

最后的代码

通过上文的分析,WeakReference弱引用可以做到不会内存泄漏。但是实际上Handler可以直接调用方法清除掉所有的回调消息。 
所以最后其实在使用Handler的时候,在生命周期的中加上一行代码,就可以了。

 @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

再看日志打印结果: 
完美的结果
完美的结果!

总结

  • 这篇博文主要思路来自与英文原文,并没有止步于理解还动手写代码。
  • 在写代码运行程序分析之后发现问题,分析弱引用的实际效果,最后的代码部分提供最终解决方案。
  • 本文仅供大家参考,我使用的Flyme系统,GC系统回收周期在5秒左右,根据不同Rom应该会有差别。

猜你喜欢

转载自blog.csdn.net/u010112268/article/details/82745389