前言
在Android
開發(fā)中署驻,內(nèi)存泄露十分常見。本文將詳細(xì)講解內(nèi)存泄露的其中一種情況:在Handler
中發(fā)生的內(nèi)存泄露
Anroid異步通信Handler系列文章
Android異步通信:Handler機(jī)制學(xué)習(xí)攻略
Android異步通信:Handler使用教程
Android異步通信:Handler工作原理
Android異步通信:Handler源碼分析
Android異步通信:詳解Handler內(nèi)存泄露的原因
目錄
背景知識(shí)
- 內(nèi)存泄露的定義:本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中
- 內(nèi)存泄露出現(xiàn)的原因:當(dāng)一個(gè)對(duì)象已經(jīng)不再被使用時(shí)健霹,本該被回收但卻因?yàn)橛辛硗庖粋€(gè)正在使用的對(duì)象持有它的引用從而導(dǎo)致它不能被回收旺上。這就導(dǎo)致了內(nèi)存泄漏。
1. 問題描述
Handler
的一般用法 = 新建Handler
子類(內(nèi)部類) 糖埋、匿名Handler
內(nèi)部類宣吱,具體如下所示。
/**
* 方式1:新建Handler子類(內(nèi)部類)
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創(chuàng)建時(shí)便自動(dòng)創(chuàng)建Looper & 對(duì)應(yīng)的MessageQueue
// 之后執(zhí)行Loop()進(jìn)入消息循環(huán)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 實(shí)例化自定義的Handler類對(duì)象->>分析1
//注:此處并無指定Looper瞳别,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper征候、MessageQueue
showhandler = new FHandler();
// 2. 啟動(dòng)子線程1
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發(fā)送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標(biāo)識(shí)
msg.obj = "AA";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
showhandler.sendMessage(msg);
}
}.start();
// 3. 啟動(dòng)子線程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發(fā)送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標(biāo)識(shí)
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
showhandler.sendMessage(msg);
}
}.start();
}
// 分析1:自定義Handler子類
class FHandler extends Handler {
// 通過復(fù)寫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內(nèi)部類
*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創(chuàng)建時(shí)便自動(dòng)創(chuàng)建Looper & 對(duì)應(yīng)的MessageQueue
// 之后執(zhí)行Loop()進(jìn)入消息循環(huán)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 通過匿名內(nèi)部類實(shí)例化的Handler類對(duì)象
//注:此處并無指定Looper,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper祟敛、MessageQueue
showhandler = new Handler(){
// 通過復(fù)寫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. 啟動(dòng)子線程1
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發(fā)送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標(biāo)識(shí)
msg.obj = "AA";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
showhandler.sendMessage(msg);
}
}.start();
// 3. 啟動(dòng)子線程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發(fā)送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標(biāo)識(shí)
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發(fā)送消息
showhandler.sendMessage(msg);
}
}.start();
}
}
-
測(cè)試結(jié)果
示意圖
- 上述例子雖可運(yùn)行成功疤坝,但代碼會(huì)出現(xiàn)嚴(yán)重警告:
- 警告的原因 = 該
Handler
類由于無設(shè)置為 靜態(tài)類,從而導(dǎo)致了內(nèi)存泄露- 最終的內(nèi)存泄露發(fā)生在
Handler
類的外部類:MainActivity
類
那么馆铁,該Handler
在無設(shè)置為靜態(tài)類時(shí)跑揉,為什么會(huì)造成內(nèi)存泄露呢?
2. 原因講解
2.1 儲(chǔ)備知識(shí)
- 主線程的
Looper
對(duì)象的生命周期 = 該應(yīng)用程序的生命周期 - 在
Java
中埠巨,非靜態(tài)內(nèi)部類 & 匿名內(nèi)部類都默認(rèn)持有 外部類的引用
2.2 泄露原因描述
從上述示例代碼可知:
- 上述的
Handler
實(shí)例的消息隊(duì)列有2個(gè)分別來自線程1历谍、2的消息(分別延遲1s
、6s
) - 在
Handler
消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)辣垒,消息隊(duì)列中的Message
持有Handler
實(shí)例的引用 - 由于
Handler
= 非靜態(tài)內(nèi)部類 / 匿名內(nèi)部類(2種使用方式)扮饶,故又默認(rèn)持有外部類的引用(即MainActivity
實(shí)例),引用關(guān)系如下圖:
- 上述的引用關(guān)系會(huì)一直保持乍构,直到
Handler
消息隊(duì)列中的所有消息被處理完畢甜无。在Handler
消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)扛点,此時(shí)若需銷毀外部類MainActivity
,但由于上述引用關(guān)系岂丘,垃圾回收器(GC)
無法回收MainActivity
陵究,從而造成內(nèi)存泄漏。如下圖:
2.3 總結(jié)
- 當(dāng)
Handler
消息隊(duì)列 還有未處理的消息 / 正在處理消息時(shí)奥帘,存在引用關(guān)系: “未被處理 / 正處理的消息 ->Handler
實(shí)例 -> 外部類” - 若出現(xiàn)
Handler
的生命周期 > 外部類的生命周期 時(shí)(即Handler
消息隊(duì)列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀時(shí))铜邮,將使得外部類無法被垃圾回收器(GC)
回收,從而造成 內(nèi)存泄露
3. 解決方案
從上面可看出寨蹋,造成內(nèi)存泄露的原因有2個(gè)關(guān)鍵條件:
- 存在 “未被處理 / 正處理的消息 ->
Handler
實(shí)例 -> 外部類” 的引用關(guān)系 -
Handler
的生命周期 > 外部類的生命周期
即
Handler
消息隊(duì)列 還有未處理的消息 / 正在處理消息 而 外部類需銷毀
解決方案的思路 = 使得上述任1條件不成立 即可松蒜。
解決方案1:靜態(tài)內(nèi)部類
原理:靜態(tài)內(nèi)部類不默認(rèn)持有外部類的引用,從而使得 “未被處理 / 正處理的消息 ->
Handler
實(shí)例 -> 外部類” 的引用關(guān)系 不存在已旧。具體方案:將
Handler
的子類設(shè)置成靜態(tài)內(nèi)部類秸苗。此外,還可使用WeakReference弱引用持有外部類运褪,保證外部類能被回收惊楼。因?yàn)椋喝跻玫膶?duì)象擁有短暫的生命周期,在垃圾回收器線程掃描時(shí)秸讹,一旦發(fā)現(xiàn)了具有弱引用的對(duì)象檀咙,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存解決代碼
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 實(shí)例化自定義的Handler類對(duì)象->>分析1
// 注:
// a. 此處并無指定Looper璃诀,故自動(dòng)綁定當(dāng)前線程(主線程)的Looper弧可、MessageQueue;
// b. 定義時(shí)需傳入持有的Activity實(shí)例(弱引用)
showhandler = new FHandler(this);
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發(fā)送的消息
Message msg = Message.obtain();
msg.what = 1;// 消息標(biāo)識(shí)
msg.obj = "AA";// 消息存放
showhandler.sendMessage(msg);
}
}.start();
}
// 設(shè)置為:靜態(tài)內(nèi)部類
private static class FHandler extends Handler{
// 定義 弱引用實(shí)例
private WeakReference<Activity> reference;
// 在構(gòu)造方法中傳入需持有的Activity實(shí)例
public FHandler(Activity activity) {
// 使用WeakReference弱引用持有Activity實(shí)例
reference = new WeakReference<Activity>(activity); }
// 通過復(fù)寫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:當(dāng)外部類結(jié)束生命周期時(shí)劣欢,清空Handler內(nèi)消息隊(duì)列
原理:不僅使得 “未被處理 / 正處理的消息 ->
Handler
實(shí)例 -> 外部類” 的引用關(guān)系 不復(fù)存在棕诵,同時(shí) 使得Handler
的生命周期(即 消息存在的時(shí)期) 與 外部類的生命周期 同步具體方案:當(dāng) 外部類(此處以
Activity
為例) 結(jié)束生命周期時(shí)(此時(shí)系統(tǒng)會(huì)調(diào)用onDestroy()
),清除Handler
消息隊(duì)列里的所有消息(調(diào)用removeCallbacksAndMessages(null)
)具體代碼
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
// 外部類Activity生命周期結(jié)束時(shí)氧秘,同時(shí)清空消息隊(duì)列 & 結(jié)束Handler生命周期
}
使用建議
為了保證Handler
中消息隊(duì)列中的所有消息都能被執(zhí)行年鸳,此處推薦使用解決方案1解決內(nèi)存泄露問題趴久,即 靜態(tài)內(nèi)部類 + 弱引用的方式
4. 總結(jié)
- 本文主要講解了
Handler
造成 內(nèi)存泄露的相關(guān)知識(shí):原理 & 解決方案 - 下一篇文章我將對(duì)講解
Android Handler
的相關(guān)知識(shí)丸相,感興趣的同學(xué)可以繼續(xù)關(guān)注Carson_Ho的簡(jiǎn)書
Anroid異步通信Handler系列文章
Android異步通信:Handler機(jī)制學(xué)習(xí)攻略
Android異步通信:Handler使用教程
Android異步通信:Handler工作原理
Android異步通信:Handler源碼分析
Android異步通信:詳解Handler內(nèi)存泄露的原因
歡迎關(guān)注Carson_Ho的簡(jiǎn)書
不定期分享關(guān)于安卓開發(fā)的干貨,追求短彼棍、平灭忠、快,但卻不缺深度座硕。