美文网首页Optimization安卓必须知道的Android开发
【译】安卓Handler当做内部类,导致内存泄露的问题

【译】安卓Handler当做内部类,导致内存泄露的问题

作者: 程序员Anthony | 来源:发表于2016-04-05 15:40 被阅读5069次

    本博客原地址:http://www.jianshu.com/p/1b39416f1508

    this handler should be static or leaks might occur
    你用android studio编译你的项目的时候可曾遇到过上面这个问题,如果有的话,这篇文章会给你解决方法。
    也是一直都会看到这个问题,但是不知道怎么解决,也不知道它描述的内存泄露的原因。知道有一天突然在statck-overflow上看见了(解决各种开发中的疑难杂症,推荐上stack-overflow。)。这里提供原文地址:inner-class-handler-memory-leak

    How to Leak a Context: Handlers & Inner Classes

    Context是怎么泄露的:Handlers & Inner Classes

    Consider the following code:

    考虑下面的代码

    public class SampleActivity extends Activity {
    
      private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
          // ... 
        }
      }
    }
    

    While not readily obvious, this code can cause cause a massive memory leak. Android Lint will give the following warning:
    尽管不是那么明显,但这段代码会导致大量内存泄露。Android的Lint工具会给出如下警告:
    In Android, Handler classes should be static or leaks might occur.
    在Android中,Handler类应该被静态修饰,否则可能会出现内存泄露。
    But where exactly is the leak and how might it happen? Let's determine the source of the problem by first documenting what we know:
    但是究竟是在哪里泄露了内存,如何泄露的,下面让我们根据已有的知识来分析下问题的原因:
    When an Android application first starts, the framework creates a Looper object for the application's main thread. A Looper implements a simple message queue, processing Message objects in a loop one after another. All major application framework events (such as Activity lifecycle method calls, button clicks, etc.) are contained inside Message objects, which are added to the Looper's message queue and are processed one-by-one. The main thread's Looper exists throughout the application's lifecycle.
    当Android应用首次启动时,framework会在应用的UI线程创建一个Looper对象。Looper实现了一个简单的消息队列并且一个接一个的处理队列中的消息。应用的所有事件(比如Activity生命周期回调方法,按钮点击等等)都会被当做一个消息对象放入到Looper的消息队列中,然后再被逐一执行。UI线程的Looper存在于整个应用的生命周期内。
    When a Handler is instantiated on the main thread, it is associated with the Looper's message queue. Messages posted to the message queue will hold a reference to the Handler so that the framework can call Handler#handleMessage(Message) when the Looper eventually processes the message.
    当在UI线程中创建Handler对象时,它就会和UI线程中Looper的消息队列进行关联。发送到这个消息队列中的消息会持有这个Handler的引用,这样当Looper最终处理这个消息的时候framework就会调用Handler#handleMessage(Message)方法来处理具体的逻辑。
    In Java, non-static inner and anonymous classes hold an implicit reference to their outer class. Static inner classes, on the other hand, do not.
    在Java中,非静态的内部类或者匿名类会隐式的持有其外部类的引用,而静态的内部类则不会。
    So where exactly is the memory leak? It's very subtle, but consider the following code as an example:
    那么,内存到底是在哪里泄露的呢?其实泄漏发生的还是比较隐晦的,但是再看看下面这段代码:

    public class SampleActivity extends Activity {
     
      private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
          // ...
        }
      }
     
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
     
        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
          @Override
          public void run() { /* ... */ }
        }, 1000 * 60 * 10);
     
        // Go back to the previous Activity.
        finish();
      }
    }
    

    When the activity is finished, the delayed message will continue to live in the main thread's message queue for 10 minutes before it is processed. The message holds a reference to the activity's Handler, and the Handler holds an implicit reference to its outer class (the SampleActivity, in this case). This reference will persist until the message is processed, thus preventing the activity context from being garbage collected and leaking all of the application's resources. Note that the same is true with the anonymous Runnable class on line 15. Non-static instances of anonymous classes hold an implicit reference to their outer class, so the context will be leaked.
    当activity被finish的时候,延迟发送的消息仍然会存活在UI线程的消息队列中,直到10分钟后它被处理掉。这个消息持有activity的Handler的引用,Handler又隐式的持有它的外部类(这里就是SampleActivity)的引用。这个引用会一直存在直到这个消息被处理,所以垃圾回收机制就没法回收这个activity,内存泄露就发生了。需要注意的是:15行的匿名Runnable子类也会导致内存泄露。非静态的匿名类会隐式的持有外部类的引用,所以context会被泄露掉
    To fix the problem, subclass the Handler in a new file or use a static inner class instead. Static inner classes do not hold an implicit reference to their outer class, so the activity will not be leaked. If you need to invoke the outer activity's methods from within the Handler, have the Handler hold a WeakReference to the activity so you don't accidentally leak a context. To fix the memory leak that occurs when we instantiate the anonymous Runnable class, we make the variable a static field of the class (since static instances of anonymous classes do not hold an implicit reference to their outer class):

    解决这个问题也很简单:在新的类文件中实现Handler的子类或者使用static修饰内部类。静态的内部类不会持有外部类的引用,所以activity不会被泄露。如果你要在Handler内调用外部activity类的方法的话,可以让Handler持有外部activity类的弱引用,这样也不会有泄露activity的风险。关于匿名类造成的泄露问题,我们可以用static修饰这个匿名类对象解决这个问题,因为静态的匿名类也不会持有它外部类的引用。

    public class SampleActivity extends Activity {
    
      /**
       * Instances of static inner classes do not hold an implicit
       * reference to their outer class.
       */
      private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;
    
        public MyHandler(SampleActivity activity) {
          mActivity = new WeakReference<SampleActivity>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
          SampleActivity activity = mActivity.get();
          if (activity != null) {
            // ...
          }
        }
      }
    
      private final MyHandler mHandler = new MyHandler(this);
    
      /**
       * Instances of anonymous classes do not hold an implicit
       * reference to their outer class when they are "static".
       */
      private static final Runnable sRunnable = new Runnable() {
          @Override
          public void run() { /* ... */ }
      };
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
        
        // Go back to the previous Activity.
        finish();
      }
    }
    

    The difference between static and non-static inner classes is subtle, but is something every Android developer should understand. What's the bottom line? Avoid using non-static inner classes in an activity if instances of the inner class could outlive the activity's lifecycle. Instead, prefer static inner classes and hold a weak reference to the activity inside.
    静态和非静态内部类的区别是非常微妙的,但这个区别是每个Android开发者应该清楚的。那么底线是什么?如果要实例化一个超出activity生命周期的内部类对象,避免使用非静态的内部类。建议使用静态内部类并且在内部类中持有外部类的弱引用。

    相关文章

      网友评论

      • 刘年年:如果是这样的话,那是不是在handler里面处理的控件都要用static修饰,感觉问题还是没有解决,网上大片的文章来这样说,可以我觉得用的时候还是很少人用吧,这种方式个人不赞成,虽然我也没有找到特别好的方法,
      • 西北狼神:为什么不使用另一种方案呢,我就一直使用另一种方案:
        1、通常情况下activity关闭都会走onDestory方法,在这里调用handler.removeAllMessage()移出所有待处理的消息即可。
        2、非通常情况,如果存在activity die了而没走onDestory,这类情况其实也根本不需要关注内存泄露的问题了。
        西北狼神:@唱过阡陌_8c89 这是之前的认知。可能只适合通常情况。第2点说的情况还涉及到系统强杀AC的结果以及被强杀后AC的内存回收问题以及java虚拟机的内存回收机制方面的。
        约你一起偷西瓜:@西北狼神 activity die了但是没走ondestory是存在于内存不够的时候自动回收 为什么就不用关心handler 了 是handler同时也被回收了?
        约你一起偷西瓜:@西北狼神 有道理 😂
      • 凤鸣游子:我有一个疑惑想请教一下. 对于Handler造成的泄露, 如果没有Looper那边对它的引用, 不是还有Activity对它有引用吗? 这样Activity和Handler互相持有引用, 会不会造成内存泄露呢? 因为他们谁也释放不掉啊.(还请解答, 困惑了好长时间了 ....)
      • xiayuexingkong:分析的很详细,以后再使用Handler处理消息的时候使用上面的方法,就解决了部分的内存泄漏的问题,, :+1:
      • fda46ca1ef80:大部分都可以在activity的ondestroy时候通过handler的removeCallbacksAndMessages(null)来防止泄露吧,除非在activity终止后还需要发送消息的,这种一般来说是不与activity关联的,完全可以用静态内部类或者独立的handler类;
        xiasuhuei321:@kevinfen9 我以前做过的几个实验是可以的,大部分都是可以通过这种方式来搞定的
      • LIU_JIAN:你好,请问下哈。如果是fragment内的handler,这个弱引用使用的是此时所依赖的activity吧?另外发现如果使用Handler mHandler =new Handler( new Handler().Callback()) 方式。用As的检测不到内存泄露问题,不知道是真有效还是无
      • coco猫:谢谢分享
      • evanwo:补充一下,内部类都会持有外部类的引用,除非使用静态内部类,重写一个handler是不能解决外部引用的问题,而要使用弱对象或者静态标志
        evanwo:@雪花鱼 对呀,内部类就是存在当前activity内,放在单独的java类中,如果不涉及到context,并不会对activity存在引用,所以不会存在内存泄漏的可能,所以我的理解没什么问题
        joychine:@雪花鱼 你的说法前一段是不对的, 重写一个handler放在单独的java文件中和直接写在当前activity中作为匿名内部类区别很大,这里涉及到是不是多产生了一个 Activity对象的问题,不过这都无关紧要。至于内存泄漏 采用弱引用才是 本文的精髓所在。 :grin:
        程序员Anthony:@雪花鱼 :+1::+1::+1:
      • evanwo:说的很好,本来只知道handler会持有activity的引用,没考虑到looper也会持有handler引用
        西北狼神:Handler、Message、MessageQueue、Looper的源码其实很少也很简单,半个小时就看完了。
        主线程的Looper是在ActivityThread.main()中的调用looper()方法的,并且ActivityThread维护有一个sMainThreadHandler的Handler实例,平常所说的所有主线程执行的地方(比如周期方法的执行)最终都是通过sMainThreadHandler来发送Message到主线程的MQ,最后由Looper中的循环取出再交给Handler的dispatchMessage方法来分发处理的。大概过程是这样。
        evanwo:@Good__Boy 是的,很多东西当年就是不知道呀
        Good__Boy:其实Looper并没有直接持有Handler的引用,而是Looper持有的MessageQueue中的Message持有Handler。
      • 1914675b3eaa:没看懂原因,静态内部类与非静态的区别
        恨自己不能小清新:@ideagit 是否存在外部类的引用呀 存在外部类的引用就可以操作外部类的成员变量 否则不能

      本文标题:【译】安卓Handler当做内部类,导致内存泄露的问题

      本文链接:https://www.haomeiwen.com/subject/oduplttx.html