Android 記憶體洩露:詳解 Handler 記憶體洩露的原因與解決方案
前言
- 在Android開發中,記憶體洩露 十分常見
1.記憶體洩露的定義:本該被回收的物件不能被回收而停留在堆記憶體中
2.記憶體洩露出現的原因:當一個物件已經不再被使用時,本該被回收但卻因為有另外一個正在使用的物件持有它的引用從而導致它不能被回收。這就導致了記憶體洩漏。
- 本文將詳細講解記憶體洩露的其中一種情況:在Handler中發生的記憶體洩露
目錄

1.png
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 = newHandler(){ // 通過複寫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類

1.png
那麼,該Handler在無設定為靜態類時,為什麼會造成記憶體洩露呢?
2. 原因講解
2.1 儲備知識
- 主執行緒的Looper物件的生命週期 = 該應用程式的生命週期
- 在Java中,非靜態內部類 & 匿名內部類都預設持有 外部類的引用
2.2 洩露原因描述
從上述示例程式碼可知:
- 上述的Handler例項的訊息佇列有2個分別來自執行緒1、2的訊息(分別 為延遲1s、6s)
- 在Handler訊息佇列 還有未處理的訊息 / 正在處理訊息時,訊息佇列中的Message持有Handler例項的引用
- 由於Handler = 非靜態內部類 / 匿名內部類(2種使用方式),故又預設持有外部類的引用(即MainActivity例項),引用關係如下圖
上述的引用關係會一直保持,直到Handler訊息佇列中的所有訊息被處理完畢

1.png
-
在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解決記憶體洩露問題,即 靜態內部類 + 弱引用的方式