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的引用,恩

還是不銷毀了。。。所以就內存泄露了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM