• Android 从java字节码告诉你 为什么Handler会造成内存泄露


    很多人面试的时候,都知道Handler 极易造成内存泄露,但是有一些讲不出来为什么,好一点的 会告诉你looper msg 之类的,但是你再往下问 为什么msg持有handler handler为什么

    持有activity'的引用的时候 他们就答不出来了。这里我通过几个简单的例子 和极少部分的源码 来帮助大家彻底理解这一个流程。

    那首先 我们来看一个例子,首先定义1个外部类 一个内部类:

     1 package com.test.zj;
     2 
     3 public class OuterClass {
     4 
     5     private int outerValue = 7;
     6     private String outerName = "outer";
     7 
     8     class InnerClass {
     9         public void printOuterValue() {
    10             System.out.println(outerName + ": " + outerValue);
    11         }
    12     }
    13 
    14 }

    然后看一下我们的主类:

     1 package com.test.zj;
     2 
     3 public class MainClass {
     4 
     5     public static void main(String[] args) {
     6         // TODO Auto-generated method stub
     7         OuterClass outerClass = new OuterClass();
     8         OuterClass.InnerClass innerClass = outerClass.new InnerClass();
     9         innerClass.printOuterValue();
    10     }
    11 
    12 }

    这个例子相信经常写android 代码的人是不会陌生的。经常会写类似的代码。那这里有没有人思考过 Outer的那2个属性 不是private的吗,为什么内部类能直接用他们呢?

    看一下字节码,首先我们看Outer的:

    所以你看这里,大家一定很奇怪,我的outer 明明只有一个构造方法啊,这里怎么多了一个access0 access1 这是什么鬼。但是继续看 发现这2个方法 一个返回string 一个返回I 也就是int,似乎我们又明白了点什么

    好继续看我们的内部类:

    注意看内部类的print方法里的字节码,重要的地方我标红了,你看,原来outer里的那2个access方法是在这里被调用的。再看那个 this$0  看下冒号后面的内容 就能明白

    这个 this$0就是指向外部类的指针啊! 所以 一个大家熟悉的概念 原理就在这了:内部类 持有外部类的引用。

    然后有人又会说了,静态内部类不会持有外部类的引用啊。好,我们现在修改一下代码 看看是否是如此:

     1 package com.test.zj;
     2 
     3 public class OuterClass {
     4 
     5     private static int outerValue = 7;
     6     private static String outerName = "outer";
     7 
     8     static class InnerClass {
     9         public void printOuterValue() {
    10             System.out.println(outerName + ": " + outerValue);
    11         }
    12     }
    13 
    14 }
     1 package com.test.zj;
     2 
     3 import com.test.zj.OuterClass.InnerClass;
     4 
     5 public class MainClass {
     6 
     7     public static void main(String[] args) {
     8         // TODO Auto-generated method stub
     9         InnerClass innerClass = new InnerClass();
    10         innerClass.printOuterValue();
    11     }
    12 
    13 }

    然后看下 字节码:

    然后看下内部类的字节码:

    你看很明显的 我们就能看到 在内部类的printf方法里  再调用外部类的属性的时候 就看不到 this0 这个指向外部类的指针了。

    回到我们的handler ,我们这个时候 就能清晰的分析出 为什么handler 有的时候会造成内存泄露了。

     1 public class MainActivity extends MainActivity{
     2 
     3     private Handler mHandler=new Handler()
     4     {
     5         public void handleMessage(Message msg)
     6         {
     7 
     8         }
     9     }
    10 
    11     protected void onCreate(Bundle saveInstance)
    12     {
    13         super.onCreate(saveInstance)
    14 
    15          mHandler.postDelayed(new Runnable() {
    16               @Override
    17               public void run() { /* ... */ }
    18             
    19             }, 1000 * 60 * 5);
    20 
    21         finish();
    22 
    23     }
    24 
    25 
    26 
    27 }

    我们来看看 这段代码为什么会造成内存泄露。

    首先 我们得明白一点,当一个app被启动的时候,android 会帮我们创建一个供ui线程使用的消息队列Looper。这个Looper就是用来处理ui线程上的事件的,

    比如什么点击事件啊,或者是我们android里面生命周期的方法啊 之类的。Looper是一条一条处理的。可不是一次性处理多条哦。可以看一下大概的源码:

     1  public static void loop() {
     2         final Looper me = myLooper();
     3         if (me == null) {
     4             throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
     5         }
     6         final MessageQueue queue = me.mQueue;
     7 
     8         // Make sure the identity of this thread is that of the local process,
     9         // and keep track of what that identity token actually is.
    10         Binder.clearCallingIdentity();
    11         final long ident = Binder.clearCallingIdentity();
    12 
    13         for (;;) {
    14             Message msg = queue.next(); // might block
    15             if (msg == null) {
    16                 // No message indicates that the message queue is quitting.
    17                 return;
    18             }
    19 
    20             // This must be in a local variable, in case a UI event sets the logger
    21             Printer logging = me.mLogging;
    22             if (logging != null) {
    23                 logging.println(">>>>> Dispatching to " + msg.target + " " +
    24                         msg.callback + ": " + msg.what);
    25             }
    26 
    27             msg.target.dispatchMessage(msg);
    28 
    29             if (logging != null) {
    30                 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    31             }
    32 
    33             // Make sure that during the course of dispatching the
    34             // identity of the thread wasn't corrupted.
    35             final long newIdent = Binder.clearCallingIdentity();
    36             if (ident != newIdent) {
    37                 Log.wtf(TAG, "Thread identity changed from 0x"
    38                         + Long.toHexString(ident) + " to 0x"
    39                         + Long.toHexString(newIdent) + " while dispatching to "
    40                         + msg.target.getClass().getName() + " "
    41                         + msg.callback + " what=" + msg.what);
    42             }
    43 
    44             msg.recycleUnchecked();
    45         }
    46     }

    一目了然 是一个循环,无限制的永远取messagequee对吧,取出来的是什么呢,废话当然是message。看27行。message对象里面有个target。

    查看message源码得知:

    1 /*package*/ Handler target;

    所谓target就是一个handler对吧。那么问题就来了,我们上面的sample代码里面 我们这个handler对象是什么啊?是一个内部类构造的对象。

    这个内部类构造的对象持有了外部类Activity的引用!所以导致 activity 无法被真正释放掉。同样的那个runnable对象实际上也是一个内部类对象,

    他也会持有activity的引用了。

    内存泄露就是在这里发生的。

    当然了,更改的方法也很简单,那就是直接把这个内部类 改成静态的不就行了!

    1 private static class MyHandler extends Handler
    2     {
    3         @Override
    4         public void handleMessage(Message msg) {
    5             super.handleMessage(msg);
    6         }
    7     }
    8 
    9     private MyHandler myHandler=new MyHandler();

    那,又有人要问了,你这个静态类,不讲道理啊,我要引用activity的属性 比如textview 啥的,引用不了啊,总不能textview 还让我弄成static变量把,

    那其实这边还是有解决方法的。

     1 private  TextView tv;
     2     private static class MyHandler extends Handler
     3     {
     4         private final WeakReference<MainActivity> mActivity;
     5         @Override
     6         public void handleMessage(Message msg) {
     7 
     8             MainActivity activity=mActivity.get();
     9             if (null!=activity)
    10             {
    11                 activity.tv.setTag("123");
    12                 super.handleMessage(msg);
    13             }
    14         }
    15 
    16         public MyHandler(MainActivity mainActivity)
    17         {
    18             this.mActivity=new WeakReference<MainActivity>(mainActivity);
    19         }
    20     }
    21 
    22     private MyHandler myHandler=new MyHandler(MainActivity.this);

    为什么要用弱引用,我解释一下,当我们activity被弹出栈以后,此时就没有强引用去指向这个activity对象了。

    如果发生gc,这个activity就会被回收,activity持有的那些资源 也自然而然就烟消云散了。对于dalivk虚拟机来说

    第一次gc 的时候 是会把 没有任何引用的对象 和 只有弱引用的对象全部回收掉的,只有当发现这2种对象全部回收掉以后

    所剩下的内存依然不够,那此时就会再进行一次gc,这时候gc 会把软引用指向的对象也回收掉。所以这里用弱引用

    是最合适的。

    你看 如果handler不做这种处理的话,我们gc的时候,一看,诶,怎么还有一个对象(handler的对象)持有activity的引用,恩

    还是不销毁了。。。所以就内存泄露了。

  • 相关阅读:
    Excel Rendering Limitations
    Output Caching and VaryByParam, VaryByCustom
    ajaxToolkit:AutoCompleteExtender 使用键值对
    Sql Server 2005 存储过程分页
    WEB前端优化
    processModel Element in Machine.config
    如何监测虚拟内存
    MemoryLimit Tuning (ASP.NET v1.1)
    缓存之SqlDependency
    SQL产生随机字符串
  • 原文地址:https://www.cnblogs.com/punkisnotdead/p/4943260.html
Copyright © 2020-2023  润新知