很多人面試的時候,都知道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的引用,恩
還是不銷燬了。。。所以就記憶體洩露了。