前言
- 在Android開發中,內存泄露 十分常見
1.內存泄露的定義:本該被回收的對象不能被回收而停留在堆內存中
2.內存泄露出現的原因:當一個對象已經不再被使用時,本該被回收但卻因為有另外一個正在使用的對象持有它的引用從而導致它不能被回收。這就導致了內存泄漏。
- 本文將詳細講解內存泄露的其中一種情況:在Handler中發生的內存泄露
目錄
1. 問題描述
Handler的一般用法 = 新建Handler子類(內部類) 、匿名Handler內部類
/**
* 方式1:新建Handler子類(內部類)
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創建時便自動創建Looper & 對應的MessageQueue
// 之后執行Loop()進入消息循環
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 實例化自定義的Handler類對象->>分析1
//注:此處并無指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue
showhandler = new FHandler();
// 2. 啟動子線程1
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標識
msg.obj = "AA";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
// 3. 啟動子線程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標識
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
}
// 分析1:自定義Handler子類
class FHandler extends Handler {
// 通過復寫handlerMessage() 從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.d(TAG, "收到線程1的消息");
break;
case 2:
Log.d(TAG, " 收到線程2的消息");
break;
}
}
}
}
/**
* 方式2:匿名Handler內部類
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創建時便自動創建Looper & 對應的MessageQueue
// 之后執行Loop()進入消息循環
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 通過匿名內部類實例化的Handler類對象
//注:此處并無指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue
showhandler = new Handler(){
// 通過復寫handlerMessage()從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.d(TAG, "收到線程1的消息");
break;
case 2:
Log.d(TAG, " 收到線程2的消息");
break;
}
}
};
// 2. 啟動子線程1
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標識
msg.obj = "AA";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
// 3. 啟動子線程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標識
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
}
}
-
測試結果
1.png - 上述例子雖可運行成功,但代碼會出現嚴重警告:
1.警告的原因 = 該Handler類由于無設置為 靜態類,從而導致了內存泄露
2.最終的內存泄露發生在Handler類的外部類:MainActivity類
那么,該Handler在無設置為靜態類時,為什么會造成內存泄露呢?
2. 原因講解
2.1 儲備知識
- 主線程的Looper對象的生命周期 = 該應用程序的生命周期
- 在Java中,非靜態內部類 & 匿名內部類都默認持有 外部類的引用
2.2 泄露原因描述
從上述示例代碼可知:
- 上述的Handler實例的消息隊列有2個分別來自線程1、2的消息(分別 為延遲1s、6s)
- 在Handler消息隊列 還有未處理的消息 / 正在處理消息時,消息隊列中的Message持有Handler實例的引用
- 由于Handler = 非靜態內部類 / 匿名內部類(2種使用方式),故又默認持有外部類的引用(即MainActivity實例),引用關系如下圖
上述的引用關系會一直保持,直到Handler消息隊列中的所有消息被處理完畢
-
在Handler消息隊列 還有未處理的消息 / 正在處理消息時,此時若需銷毀外部類MainActivity,但由于上述引用關系,垃圾回收器(GC)無法回收MainActivity,從而造成內存泄漏。如下圖:
1.png
2.3 總結
當Handler消息隊列 還有未處理的消息 / 正在處理消息時,存在引用關系: “未被處理 / 正處理的消息 -> Handler實例 -> 外部類”
若出現 Handler的生命周期 > 外部類的生命周期 時(即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀時),將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露
3. 解決方案
從上面可看出,造成內存泄露的原因有2個關鍵條件:
- 存在“未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關系
- Handler的生命周期 > 外部類的生命周期
即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀
解決方案的思路 = 使得上述任1條件不成立 即可。
解決方案1:靜態內部類+弱引用
- 原理
靜態內部類 不默認持有外部類的引用,從而使得 “未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關系 的引用關系 不復存在。 - 具體方案
將Handler的子類設置成 靜態內部類
同時,還可加上 使用WeakReference弱引用持有Activity實例
原因:弱引用的對象擁有短暫的生命周期。在垃圾回收器線程掃描時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存
- 解決代碼
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創建時便自動創建Looper & 對應的MessageQueue
// 之后執行Loop()進入消息循環
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 實例化自定義的Handler類對象->>分析1
//注:
// a. 此處并無指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue;
// b. 定義時需傳入持有的Activity實例(弱引用)
showhandler = new FHandler(this);
// 2. 啟動子線程1
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標識
msg.obj = "AA";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
// 3. 啟動子線程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標識
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
}
// 分析1:自定義Handler子類
// 設置為:靜態內部類
private static class FHandler extends Handler{
// 定義 弱引用實例
private WeakReference<Activity> reference;
// 在構造方法中傳入需持有的Activity實例
public FHandler(Activity activity) {
// 使用WeakReference弱引用持有Activity實例
reference = new WeakReference<Activity>(activity); }
// 通過復寫handlerMessage() 從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.d(TAG, "收到線程1的消息");
break;
case 2:
Log.d(TAG, " 收到線程2的消息");
break;
}
}
}
}
解決方案2:當外部類結束生命周期時,清空Handler內消息隊列
- 原理
不僅使得 “未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關系 不復存在,同時 使得 Handler的生命周期(即 消息存在的時期) 與 外部類的生命周期 同步 - 具體方案
當 外部類(此處以Activity為例) 結束生命周期時(此時系統會調用onDestroy()),清除 Handler消息隊列里的所有消息(調用removeCallbacksAndMessages(null)) - 具體代碼
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
// 外部類Activity生命周期結束時,同時清空消息隊列 & 結束Handler生命周期
}
使用建議
為了保證Handler中消息隊列中的所有消息都能被執行,此處推薦使用解決方案1解決內存泄露問題,即 靜態內部類 + 弱引用的方式