一次性講清楚 Handler 可能導致的記憶體洩漏和解決辦法 | 開發者說·DTalk

語言: CN / TW / HK

本文原作者: 小蝦米君, 原文 釋出於: TechMerger

本文重製和補充了多個示意圖和章節, 期望能為您一次性講清楚 Handler 可能導致的記憶體洩漏和解決辦法!

  1. Handler 使用不當?

  2. 為什麼會記憶體洩露?

  3. 子執行緒 Looper 會導致記憶體洩露嗎?

  4. 非內部類的 Handler 會記憶體洩露嗎?

  5. 網傳的 Callback 介面真得能解決嗎?

  6. 正確使用 Handler?

  7. 結語

Handler 使用不當?

先搞清楚什麼叫 Handler 使用不當

一般具備這麼幾個特徵:

1. Handler 採用 匿名內部類內部類 擴充套件,預設持有外部類 Activity 的引用:

// 匿名內部類
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Anonymous inner handler message occurred & what:${msg.what}"
)
}
}
}
// 內部類
override fun onCreate(savedInstanceState: Bundle?) {
...
val innerHandler: Handler = MyHandler(Looper.getMainLooper())
}


inner class MyHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
Log.d(
"MainActivity",
"Inner handler message occurred & what:\${msg.what}"
)
}
}

2. Activity 退出的時候 Handler 仍可達,有兩種情況:

  • 退出的時候仍有 Thread 在處理中,其引用著 Handler;

  • 退出的時候雖然 Thread 結束了,但 Message 尚在佇列中排隊處理正在處理中 ,間接持有 Handler。

override fun onCreate(savedInstanceState: Bundle?) {
...
val elseThread: Thread = object : Thread() {
override fun run() {
Log.d(
"MainActivity",
"Thread run"
)


sleep(2000L)
innerHandler.sendEmptyMessage(1)
}
}.apply { start() }
}

為什麼會記憶體洩露?

上述的 Thread 在執行的過程中,如果 Activity 進入了後臺,後續因為記憶體不足觸發了 destroy 。虛擬機器在標記 GC 物件的時候,會發生如下兩種情形:

  • Thread 尚未結束,處於活躍狀態

    活躍的 Thread 作為 GC Root 物件,其持有 Handler 例項,Handler 又預設持有外部類 Activity 的例項,這層引用鏈仍可達:

  • Thread 雖然已結束,但傳送的 Message 還未處理完畢

    Thread 傳送的 Message 可能還在佇列中等待,又或者正好處於 handleMessage() 的回調當中。此刻 Looper 通過 MessagQueue 持有該 Message,Handler 又作為 target 屬性被 Message 持有,Handler 又持有 Activity,最終導致 Looper 間接持有 Activity。

    大家可能沒有注意到主執行緒的 Main Looper 是不同於其他執行緒的 Looper 的。

    為了能夠讓任意執行緒方便取得主執行緒的 Looper 例項,Looper 將其定義為了靜態屬性 sMainLooper

public final class Looper {
private static Looper sMainLooper; // guarded by Looper.class
...
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
sMainLooper = myLooper();
}
}
}

靜態屬性也是 GC Root 物件,其通過上述的應用鏈導致 Activity 仍然可達。

這兩種情形都將導致 Activity 例項將無法被正確地標記,直到 Thread 結束且 Message 被處理完畢。在此之前 Activity 例項將得不到回收。

內部類 Thread 也會導致 Activity 無法回收吧?

為了側重闡述 Handler 導致的記憶體洩漏,並沒有針對 Thread 直接產生的引用鏈作說明。

上面的程式碼示例中 Thread 也採用了匿名內部類形式,其當然也持有 Activity 例項。從這點上來說,尚未結束的 Thread 會直接佔據 Acitvity 例項,這也是導致 Activity 記憶體洩露的一條引用鏈,需要留意!

子執行緒 Looper 會導致記憶體洩露嗎?

為了便於每個執行緒方便拿到獨有的 Looper 例項,Looper 類採用靜態的 sThreadLocal 屬性 管理 著各例項。

public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
...
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
}

可是作為靜態屬性的話,sThreadLocal 也是 GC Root 物件。那從這個角度講會不會也間接導致 Message 無法回收呢?

會,但本質上不是因為 ThreadLocal,而是因為 Thread。

翻看 ThreadLocal 的原始碼,您會發現: 目標物件並不存放在 ThreadLocal 中,而是其靜態內部類 ThreadLocalMap 中。加上為了執行緒獨有,該 Map 又被 Thread 持有, 兩者的生命週期等同

// TheadLocal.java
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}


// 建立 Map 並放到了 Thread 中
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
// Thread.java
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

更進一步的細節是: ThreadLocalMap 中的元素 Entry 採用 弱引用 持有作為 key 的 ThreadLocal 物件,但作為 value 的目標物件則被 強引用 著。

這就導致 Thread 間接持有著目標物件,比如本次的 Looper 例項。這樣可以確保 Looper 的生命週期和 Thread 保持一致,但 Looper 生命週期過長會有記憶體洩漏的風險 (當然這不是 Looper 設計者的鍋)。

// TheadLocal.java
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
private Entry[] table;


static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;


Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}

當弱引用的 key 例項因為 GC 發生會被切斷和 Map 的引用關係,但直到下一次手動執行 Entry 的 set get remove 前,value 都沒被置為 null 。這段時間 value 都被強引用著,造成隱患。

The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).

Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table.

Such entries are referred to as "stale entries" in the code that follows.

但需要注意的是: 本處 Looper 持有的例項 sThreadLocal 是靜態的,程序結束之前都不會被回收, 不可能發生上述提及的 key 被回收 value 被孤立的情況

簡言之,Looper 的 ThreadLocal 不會導致記憶體洩漏。

回到 Thread,因其強引用 Looper,所以 Thread 仍然會因為這個因素導致 Message 發生記憶體洩漏。 比如: 向子執行緒 Handler 傳送了內部類寫法的 Runnable,當 Activity 結束的時候該 Runnable 尚未抵達或正在執行過程中,那麼 Activity 會因為如下的引用鏈一直可達,即發生記憶體洩漏的可能。

解決辦法不復雜:

  • Activity 結束的時候移除子執行緒 Handler 中所有未處理 Message,比如 Handler#removeCallbacksAndMessages() 。這將會切斷 MessageQueue 到 Message,以及 Message 到 Runnable 的引用關係;

  • 更好的辦法是呼叫 Looper#quit() quitSafely() ,它將清空所有的 Message 或未來的 Message,並促使 loop() 輪詢的結束。子執行緒的結束則意味著引用起點 GC Root 不復存在。

最後,明確幾點共識:

  1. 管理 Looper 例項的靜態屬性 sThreadLocal,並不持有實際存放 Looper 的 ThreadLocalMap,而是通過 Thread 去讀寫。這使得 sThreadLocal 雖貴為 GC Root,但無法達成到 Looper 的引用鏈,進而從這條路徑上並不能構成記憶體洩漏!

  2. 另外,因其是靜態屬性, 也不會發生 key 被回收 value 被孤立的記憶體洩漏風險

  3. 最後,由於 Thread 持有 Looper value,從這條路徑上來說 是存在記憶體洩漏的可能的

非內部類的 Handler 會記憶體洩露嗎?

上面說過匿名內部類或內部類是 Handler 造成記憶體洩漏的一個特徵,那如果 Handler 不採用內部類的寫法,會造成洩露嗎?

比如這樣:

override fun onCreate(...) {
Handler(Looper.getMainLooper()).apply {
object : Thread() {
override fun run() {
sleep(2000L)
post {
// Update ui
}
}
}.apply { start() }
}
}

仍然可能造成記憶體洩漏。

雖然 Handler 不是內部類,但 post 的 Runnable 也是內部類,其同樣會持有 Activity 的例項。另外,post 到 Handler 的 Runnable 最終會作為 callback 屬性被 Message 持有。

基於這兩個表現,即便 Handler 不是內部類了,但因為 Runnable 是內部類,同樣會發生 Activity 被 Thread 或 Main Looper 不當持有的風險。

網傳的 Callback 介面真得能解決嗎?

網上有種說法: 建立 Handler 時不覆寫 handleMessage(),而是指定 Callback 介面例項,這樣子可以避免記憶體洩漏。理由是這種寫法之後 AndroidStudio 就不會再彈出如下的警告:

This Handler class should be static or leaks might occur.

事實上,Callback 例項如果仍然是匿名內部類或內部類的寫法,仍然會造成記憶體洩漏,只是 AS 沒彈出這層警告而已。

private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return false;
}
});

比如上面的這種寫法,Handler 會持有傳遞進去的 Callback 例項,而 Callback 作為內部類寫法,預設持有外部類 Activity 的引用。

public class Handler {
final Callback mCallback;


public Handler(@NonNull Looper looper, @Nullable Callback callback) {
this(looper, callback, false);
}


public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
...
mCallback = callback;
}
}

無論是從 Thread 活躍的角度,還是從 Thread 結束但 Message 仍然未執行完的角度來說,都將導致 Activity 仍然可被 GC Root 間接引用而發生記憶體洩漏的風險。

本質來說和上面的 Runnable 例子是一樣的問題:

正確使用 Handler?

GC 標記的時候 Thread 已結束並且 Message 已被處理的條件一旦沒有滿足,Activity 的生命週期就將被錯誤地延長,繼而引發記憶體洩露!

那如何避免這種情況的發生呢?針對上面的特徵,其實應該已經有了答案:

  • 一則將強引用 Activity 改為弱引用;

  • 二則及時地切斷兩大 GC Root 的引用鏈關係: Main Looper 到 Message,以及結束子執行緒。

程式碼示例簡述如下:

1. 將 Handler 或 Callback 或 Runnable 定義為 靜態內部類 :

class MainActivity : AppCompatActivity() {
private class MainHandler(looper: Looper?, referencedObject: MainActivity?) :
WeakReferenceHandler<MainActivity?>(looper, referencedObject) {
override fun handleMessage(msg: Message) {
val activity: MainActivity? = referencedObject
if (activity != null) {
// ...
}
}
}
}

2. 還需要 弱引用 外部類的例項:

open class WeakReferenceHandler<T>(looper: Looper?, referencedObject: T) :     Handler(looper!!) {
private val mReference: WeakReference<T> = WeakReference(referencedObject)


protected val referencedObject: T?
protected get() = mReference.get()
}

3. onDestroy 的時候 切斷引用鏈關係 ,糾正生命週期:

  • Activity 銷燬的時候,如果子執行緒任務尚未結束,及時 中斷 Thread:

override fun onDestroy() {
...
thread.interrupt()
}
  • 如果子執行緒中建立了 Looper 併成為了 Looper 執行緒的話,須 手動 quit 。比如 HandlerThread :

override fun onDestroy() {
...
handlerThread.quitSafely()
}
  • 主執行緒的 Looper 無法手動 quit,所以還需手動 清空主執行緒中 Handler 未處理的 Message :

override fun onDestroy() {
...
mainHandler.removeCallbacksAndMessages(null)
}

※1: Message 在執行 recycle() 後會清除其與 Main Handler 的引用關係;

※2: Looper 子執行緒呼叫 quit 時會清空 Message,所以無需針對子執行緒的 Handler 再作 Message 的清空處理了。

結語

回顧一下本文的幾個要點:

  • 持有 Activity 例項的 Handler 處理,其 生命週期 應當和 Activity 保持一致;

  • 如果 Activity 本該銷燬了,但非同步 Thread 仍然活躍或傳送的 Message 尚未處理完畢,將導致 Activity 例項的 生命週期被錯誤地延長;

  • 造成本該回收的 Activity 例項 被子執行緒或  Main Looper 佔據而無法及時回收。

簡單來講的正確做法:

  • 使用 Handler 機制的時候,無論是覆寫 Handler 的 handleMessage() 方式,還是指定回撥的 Callback 方式,以及傳送任務的 Runnable 方式,儘量採用 靜態內部類 + 弱引用 ,避免其強引用持有 Activity 的例項。

    確保即便錯誤地延長了生命週期,Activity 也能及時被 GC 回收。

  • 同時在 Activity 結束的時候,及時地 清空 Message、終止 Thread 或退出 Looper ,以便回收 Thread 或 Message。

    確保能徹底切斷 GC Root 抵達 Activity 的引用鏈。

長按右側二維碼

檢視更多開發者精彩分享

"開發者說·DTalk" 面向 中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平臺。我們將通過大家的技術內容著重選出優秀案例進行 谷歌開發技術專家 (GDE) 的推薦。

  點選屏末  |  閱讀原文  | 即刻報名參與  " 開發者說 · DTalk"