有關Android Handler記憶體洩漏分析及解決辦法
1、Android的開發工具是java,這能幫助我們解決很底層的問題 包括:記憶體管理,平臺依賴。然而,有時候專案依然會報OOM錯誤,so垃圾收集器在哪?
2、我主要研究一種情況:記憶體中較大物件很長一段時間內不能被釋放。這方面並不完全算作記憶體溢位,物件會在某一時間點上被收集,so我們不屌它。雖然有時候他也會導致oom,所以不建議這麼幹滴。(這話咋說的這麼矛盾,作者精分了?)
3、簡單例子:
public class NewActivity extends Activity { private Handler mHandler = new Handler(); private TextView mTextview; @Override protected void onCreate(android.os.Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.inflate_textview); mTextview = (TextView) findViewById(R.id.xxwj_newtv4); mHandler.postDelayed(new Runnable() { @Override public void run() { mTextview.setText("Done"); } }, 80000); }; }
4、這是一個非常基本的Activity,注意到匿名類Runnable被handler延遲執行了好多次,我們執行並且旋轉螢幕多次,然後dump memory 分析它
5、現在記憶體裡有很多Activity,這tm很不好啊,為啥gc沒收了他們。
6、獲取所有記憶體的Activity查詢語句是在OQL(Object Query Language)建立的,非常簡單粗暴。
7、能夠看出,其中一個Activity是被this$0引用了,這是一個來自內部匿名類指向其主類的間接引用(OMG) this$0是被callback方法引用的,callback又被一堆next()方法引用到主執行緒。
8、任何時候你建立一個非靜態內部類在你自己的類中,java會自動為其建立一個間接引用指向其主類。
9、只要你的handler呼叫了Runnable或Message,它將被儲存在LoopThread所引用的Message訊息佇列中,直到Message被執行。傳送delayed訊息是一個明顯的記憶體洩漏,洩漏的時間大於等於延遲的時間。如果訊息佇列很長的話傳送非延遲訊息也可能引發一個臨時洩漏。
10、解決方案1:Static Runnable
讓我們試著克服這個把我們弄瘋了的this$0,將匿名類轉化為靜態類
@Override
protected void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.inflate_textview);
mTextview = (TextView) findViewById(R.id.xxwj_newtv4);
mHandler.postDelayed(new DoneRunnable(mTextview), 80000);
};
private static final class DoneRunnable implements Runnable{
private final TextView mTV;
public DoneRunnable(TextView tv) {
this.mTV = tv;
}
@Override
public void run() {
mTV.setText("Done");
}
}
11、執行、旋轉螢幕、釋放記憶體堆:
12、啥?還tm這樣,看看誰持有了Activities的引用
13、看這顆樹的最底部,activity被DoneRunnable中的mTextView類的mContext引用,用靜態內部類沒辦法解決記憶體洩漏,需要來點更狠的
14、解決方案2:弱引用的Static Runnable
我們繼續用之前的修復辦法並且弱引組織Activity被釋放的TextView
@Override
protected void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.inflate_textview);
mTextview = (TextView) findViewById(R.id.xxwj_newtv4);
mHandler.postDelayed(new DoneRunnable(mTextview), 80000);
};
private static final class DoneRunnable implements Runnable{
private final WeakReference<TextView> mTV;
public DoneRunnable(TextView tv) {
this.mTV = new WeakReference<TextView>(tv);
}
@Override
public void run() {
final TextView tt = mTV.get();
if(tt != null){
tt.setText("Done");
}
}
}
15、重新執行,旋轉,釋放記憶體:
16、woops!只有一個Activity例項,解決了我們的記憶體洩漏問題
17、使用弱引用時要小心謹慎,他們能在任何時候被置為空,解決辦法就是先賦值給一個區域性變數(強引用),然後在使用前檢查是否為空
18、解決handler記憶體洩漏我們需要做的:
1、用靜態內部類或外部類
2、操縱Handler/Runnable時用弱引用
19、如果和一開始的程式碼進行對比,會發現在可讀性和清晰度方面有很大的不同。一開始的程式碼很短和清晰,這麼寫太麻煩
20、解決方案3:在onDestroy中清空所有Message
Handler類中有個牛逼方法removeCallbacksAndMessages(),它可以接受空欄位作為引數,它將移除所有特定Handler的Runnable和Message:
@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
21、完美!這比上一個解決辦法要好很多。唯一難搞的是你需要在所有的Activity/Fragment中呼叫onDestory()方法來清除message,聽著都噁心
22、解決方案4:用WeakHandler
隆重介紹一下Badoo團隊整的WeakHeadler,Handler的替代方案,安全多了。
23、程式碼很像吧,經實測記憶體中也只有一個例項,簡單吧,程式碼和記憶體都一樣簡潔。使用方法gradle一下即可
24、總結:
本文一共介紹了3種方案:
1、宣告一個繼承Runnable類的靜態內部類,並弱引用要操作的控制元件
2、在onDestroy()中呼叫removeCallbacksAndMessages()
3、呼叫Badoo團隊的WeakHandler來替代os.Handler
666、WeakHandler只能替代postDelay(New Runnable())方法造成的記憶體洩漏,沒辦法替代os.Handler中接收訊息的操作,所以正常情況下需要呼叫方法(2)