深入理解 Android Handler

一、引言

Handler 在安卓中的地位是不言而喻的,幾乎維系著整個安卓程序運行的生命周期,但是這么重要的一個東西,我們真的了解它嗎?下面跟隨著我的腳步,慢慢揭開Hanler的神秘面紗吧!

本文將介紹Handler 的運行機制、MessageQueue、Looper 的關系,ThreadLocal,以及Handler 導致的內存泄漏問題


二、Handler 系統組成概覽

Handler 的源碼中,主要涉及以下核心組件:

  • Message:封裝消息的數據結構。
  • MessageQueue:存儲 Message 的隊列,內部是單鏈表
  • Looper:負責循環讀取 MessageQueue 并分發消息。
  • Handler:對外提供 sendMessage()post() 發送消息,并處理 MessageQueue 中的消息。

它們之間關系如下圖所示:


三、Handler 的創建

Handler 被創建時,它會綁定當前線程的 Looper

public Handler() {this(Looper.myLooper(), null, false);
}
public Handler(Looper looper) {this(looper, null, false);
}

最終調用:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async,boolean shared) {mLooper = looper;mQueue = looper.mQueue;mCallback = callback;mAsynchronous = async;mIsShared = shared;
}
  • mLooper 通過 Looper.myLooper() 獲取當前線程的 Looper
  • mQueueLooper 提供,確保所有 Handler 在同一個 Looper 線程內共享 MessageQueue

重點:主線程默認初始化 Looper,但子線程默認沒有,需要手動 Looper.prepare()

如果一定要在子線程中使用,推薦使用 HandlerThread,比于手動創建 LooperHandlerThread 封裝了 Looper 的創建和管理邏輯,代碼更加簡潔,也更易于維護。同時,HandlerThread 有自己獨立的消息隊列,不會干擾主線程或其他線程的消息處理。


四、sendMessage() 如何發送消息

當我們調用 sendMessage() 時:

handler.sendMessage(msg);

實際上調用:

public boolean sendMessage(Message msg) {return sendMessageDelayed(msg, 0);
}

最終:

public boolean sendMessageDelayed(Message msg, long delayMillis) {return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

最終調用 enqueueMessage()

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; // 綁定 Handlerreturn queue.enqueueMessage(msg, uptimeMillis);
}
@UnsupportedAppUsage
/*package*/ Handler target;

也就是說 Message 引用了 Handler,這也為內存泄漏埋下伏筆


五、MessageQueue 插入消息

boolean enqueueMessage(Message msg, long when) {synchronized (this) {// 插入 MessageQueueMessage prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}}return true;
}

enqueueMessage 方法負責將消息按照時間順序正確地插入到單鏈表結構的隊列中,按 when 進行排序。


六、Looper 如何處理消息

Looper.loop() 讀取消息
public static void loop() {for (;;) {Message msg = queue.next(); // 取出消息//...msg.target.dispatchMessage(msg); // 交給 Handler 處理}
}
MessageQueue.next()
Message next() {// 檢查消息隊列是否已銷毀,若銷毀則返回 nullif (mPtr == 0) return null;int nextPollTimeoutMillis = 0;for (;;) {// 若有超時時間,刷新 Binder 待處理命令if (nextPollTimeoutMillis != 0) Binder.flushPendingCommands();// 阻塞線程,等待新消息或超時nativePollOnce(mPtr, nextPollTimeoutMillis);synchronized (this) {final long now = SystemClock.uptimeMillis();Message msg = mMessages;// 若為屏障消息,找下一個異步消息if (msg != null && msg.target == null) {do { msg = msg.next; } while (msg != null && !msg.isAsynchronous());}if (msg != null) {// 若消息未到處理時間,計算超時時間if (now < msg.when) {nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// 若消息到處理時間,從隊列移除并返回mMessages = msg.next;msg.next = null;msg.markInUse();return msg;}} else {// 若無消息,一直阻塞nextPollTimeoutMillis = -1;}// 若消息隊列正在退出,釋放資源并返回 nullif (mQuitting) {dispose();return null;}}}
}

nativePollOnce() 讓當前線程進入阻塞狀態,直到有新的消息到來或者超時

nativePollOnce() 的主要功能是:

  • 線程阻塞:讓當前線程進入等待狀態,避免空轉消耗CPU資源
  • 事件喚醒:當有新消息到達或超時發生時,立即喚醒線程處理
  • Native 層集成:與 Linux 的 epoll 機制對接,實現高效I/O多路復用
void nativePollOnce(long ptr, int timeoutMillis)

ptr:指向 Native Looper 對象的指針(C++對象地址)

timeoutMillis 的含義:

  • 如果 timeoutMillis > 0
    • epoll_wait 最多阻塞 timeoutMillis 毫秒,期間如果有事件發生,則提前返回。
  • 如果 timeoutMillis == 0
    • epoll_wait 立即返回(非阻塞)。
  • 如果 timeoutMillis < 0
    • epoll_wait 無限等待,直到有事件觸發。

最終調用了 Linux epoll 機制 來監聽消息事件。


七、nativePollOnce 方法調用流程

Java 層調用
// MessageQueue.java
private native void nativePollOnce(long ptr, int timeoutMillis);

JNI 本地方法,由 MessageQueue 調用,用于等待消息。

MessageQueue.next() 方法中:

// MessageQueue.java
nativePollOnce(mPtr, nextPollTimeoutMillis);

它的作用是:

  • 如果 MessageQueue 里有消息,立即返回。
  • 如果沒有消息,則阻塞,直到有新的消息到來或 timeoutMillis 超時。
JNI 層調用
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,jlong ptr, jint timeoutMillis) {MessageQueue* mq = reinterpret_cast<MessageQueue*>(ptr);mq->pollOnce(timeoutMillis);
}

將 Java 傳來的 mPtr 轉換成 MessageQueue* 對象,并調用 pollOnce() 方法。

Native 層 pollOnce()

MessageQueue.cpp

void MessageQueue::pollOnce(int timeoutMillis) {mLooper->pollOnce(timeoutMillis);
}

調用了 Looper::pollOnce(),進入 消息輪詢 邏輯。

Looper 的 pollOnce()

Looper.cpp

int Looper::pollOnce(int timeoutMillis) {return pollInner(timeoutMillis);
}

這里調用 pollInner(timeoutMillis),它的核心邏輯是 使用 epoll_wait() 監聽事件

epoll 監聽消息事件

pollInner(timeoutMillis) 的核心邏輯:

int Looper::pollInner(int timeoutMillis) {struct epoll_event eventItems[EPOLL_MAX_EVENTS];int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);if (eventCount > 0) {for (int i = 0; i < eventCount; i++) {// 處理事件}}
}

其中:

  • mEpollFdepoll 文件描述符,用于監聽多個文件描述符(FD)。
  • epoll_wait()阻塞當前線程,直到:
    • 新消息可讀
    • 文件描述符事件觸發
    • 超時timeoutMillis 毫秒后自動返回)

到這里,我們清楚了 nativePollOnce 的主要作用是等待新消息到達消息隊列。當調用這個方法時,如果當前消息隊列中沒有需要立即處理的消息,線程會被阻塞,從而釋放 CPU 資源,直到有新消息到來或者發生其他喚醒條件。

那么 epoll_wait() 如何監聽消息?
epoll_wait() 監聽哪些事件?

MessageQueue 的 pipe(管道):當 Handler 發送消息時,寫入 pipe,觸發 epoll 事件。

輸入事件:當用戶觸摸屏幕或按鍵時,觸發 epoll 事件。

文件描述符(FileDescriptor):例如 Binder 進程間通信(IPC)事件。

等等…

消息如何觸發 epoll?
  • Handler.sendMessage() 會向 MessageQueue 寫入數據:
  write(mWakeEventFd, "W", 1);
  • epoll_wait() 監聽到 pipe 有數據,返回。

  • Looper 處理新消息,Java 層 Handler 開始執行 handleMessage()

epoll_wait阻塞等待wakeFd上的可讀事件,當有數據寫入wakeFdepoll_wait返回,線程被喚醒,這里并不關心寫入wakeFd的具體數據是什么,只關心可讀事件的發生

pipe 的作用

Handler.sendMessage() 觸發 epoll 事件,立即喚醒 Looper

至此,綜上,我們可以知道 epoll_wait() 只負責等待事件,不會提前返回“第一條消息”,它只會返回“有事件觸發”的信號,具體執行哪個消息是 MessageQueue.next() 的邏輯,它會選擇最早應該執行的消息,這就是 Handler 的阻塞喚醒的核心邏輯所在!


八、Handler 處理消息

public void dispatchMessage(Message msg) {if (msg.callback != null) {msg.callback.run();} else {handleMessage(msg);}
}

最終執行:

@Override
public void handleMessage(Message msg) {// 需要用戶實現
}

九、核心組件之間的關系

Thread└── ThreadLocal<Looper>└── Looper└── MessageQueue└── Message1 → Message2 → ...↑Handler
  • Handler 持有對 MessageQueue 的引用(間接通過 Looper)因為Handler中的 MessageQueue 是從 Looper 中獲取的;
    public Handler(@Nullable Callback callback, boolean async) {//..mQueue = mLooper.mQueue;//..}
  • 每個線程通過 ThreadLocal 綁定自己的 Looper;
  • Looper 管理其對應的 MessageQueue;

這樣它們的關系就清晰了,每個線程只有一個Looper(是由ThreadLocal確保的),可以有多個Handler。

public final class Looper {// 線程本地存儲,每個線程一個Looper實例static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();}public static void prepare() {prepare(true);}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get()!= null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}
}

關于ThreadLocal的詳細介紹可以看這篇文章:深入剖析Java中ThreadLocal原理


十、內存泄漏問題分析及解決方案

我們都知道判斷內存泄漏的依據是:短生命周期對象是否被長生命周期對象引用!既然使用Handler不當會導致內存泄漏,那么我們只需要找到被引用的源頭,然后去解決。

Handler 導致內存泄漏的完整引用流程
  • 匿名內部類或非靜態內部類的隱式引用

眾所周知,在Java中 匿名內部類或非靜態內部類會持有外部類的引用,如下:

public class MainActivity extends AppCompatActivity {private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 處理消息}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mHandler.sendEmptyMessageDelayed(0, 10000);}
}

這里的mHandler是一個非靜態內部類。非靜態內部類會隱式持有外部類(這里是MainActivity)的引用。這意味著mHandler對象中包含了對MainActivity實例的引用。

  • MessageQueue 對 Message 的持有

在上面示例中,我們發送了一個延遲的Message,盡管只傳了一個0,但是其內部也會封裝為Message,這時候Handler 會將 Message對象并將其發送到與之關聯的MessageQueue中,MessageQueue會持有這個Message對象,直到該消息被處理。

  • Message 對 Handler 的持有

由上面第四小節的sendMessage()可知,在放入隊列的時候,會將HandlerMessage 關聯:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; // 綁定 Handlerreturn queue.enqueueMessage(msg, uptimeMillis);
}

主要作用是,讓Message知道是從哪個Handler發送的,并最終讓那個HandlerhandleMessage去處理。

public final class Looper {static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();@UnsupportedAppUsageprivate static Looper sMainLooper;  // guarded by Looper.class//...
}

我們都知道,在主線程中,主線程的Looper會一直運行下去(或者說 Looper被 靜態 ThreadLocal<Looper> 所引用),不能被停止,而MessageQueue 又被Looper 所引用,這就產生了一條完整的引用鏈:ThreadLocal<Looper> - Looper - MessageQueue - Message - Handler - MainActivity

** 解決方案**
  • 使用靜態內部類 + WeakReference:

要解決內存泄漏,就是把引用鏈上任意一條引用斷開,讓GC不可達就行了,其實我們能操作的就只有 Handler - **MainActivity **這一條引用:

static class MyHandler extends Handler {private final WeakReference<MyActivity> ref;MyHandler(MyActivity activity) {ref = new WeakReference<>(activity);}@Overridepublic void handleMessage(Message msg) {MyActivity activity = ref.get();if (activity != null) {// Safe to use activity}}
}
  • 在 Activity 的 onDestroy() 中清除消息:
handler.removeCallbacksAndMessages(null);

其實,只要消息不是延遲很久或者反復堆積,就不會在 MessageQueue 中長時間滯留,從而也就不會延長 Handler 或其持有對象的生命周期。

想想,在實際開發中,誰會在Activity中延遲發送一個很長時間的消息,所以我們不必為 Handler 導致內存泄漏,過度緊張,稍微留意一下就可以避免了 😃


十一、最后

Handler 是 Android 消息機制的基礎組成部分。通過對 Handler、Looper、MessageQueue 之間關系的理解,我們可以更深入掌握 Android 的線程模型與 UI 更新流程。

由于本人能力有限,并沒有對 Handler 進行過度深入全面了解,比如同步屏障等,如果文章內容解讀有誤,還望不吝賜教。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/77247.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/77247.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/77247.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

讀書筆記 -- MySQL架構

1、MySQL邏輯架構 最上層的服務并不是 MySQL所獨有的&#xff0c;大多數基于網絡的客戶端/服務器的工具或者服務都有類似的架構。比如連接處理、授權認證、安全等等。 第二層架構是 MySQL 比較有意思的部分。大多數 MySQL 的核心服務功能都在這一層包括查詢解析、分析、…

linux 4.14內核jffs2文件系統不自動釋放空間的bug

前段時間在做spi-nor flash項目的時候&#xff0c;使用jffs2文件系統&#xff0c;發現在4.14內核下存在無法釋放空間的bug&#xff0c;后來進行了修復&#xff0c;修復后功能正常&#xff0c;現將修復patch公開&#xff0c;供后來者學習&#xff1a; diff --git a/fs/jffs2/ac…

vue3+vite 實現.env全局配置

首先創建.env文件 VUE_APP_BASE_APIhttp://127.0.0.1/dev-api 然后引入依賴&#xff1a; pnpm install dotenv --save-dev 引入完成后&#xff0c;在vite.config.js配置文件內加入以下內容&#xff1a; const env dotenv.config({ path: ./.env }).parsed define: { // 將…

Oracle 19c部署之手工建庫(四)

#Oracle #19c #手工建庫 手工創建Oracle數據庫&#xff08;也稱為手工建庫&#xff09;是指在已經安裝了Oracle數據庫軟件的基礎上&#xff0c;通過手動執行一系列命令和步驟來創建一個新的數據庫實例。這種方法與使用Database Configuration Assistant (DBCA)等工具自動創建數…

【Reading Notes】(8.3)Favorite Articles from 2025 March

【March】 雷軍一度登頂中國首富&#xff0c;太厲害了&#xff08;2025年03月02日&#xff09; 早盤&#xff0c;小米港股一路高歌猛進&#xff0c;暴漲4%&#xff0c;股價直接飆到52港元的歷史新高。這一波猛如虎的操作&#xff0c;直接把雷軍的身家拉到了2980億元&#xff0c…

【Python爬蟲基礎篇】--1.基礎概念

目錄 1.爬蟲--定義 2.爬蟲--組成 3.爬蟲--URL 1.爬蟲--定義 網絡爬蟲&#xff0c;是一種按照一定規則&#xff0c;自動抓取互聯網信息的程序或者腳本。另外一些不常使用的名字還有螞蟻、自動索引、模擬程序或者蠕蟲。隨著網絡的迅速發展&#xff0c;萬維網成為大量信息的載體…

C語言超詳細結構體知識

1.自定義類型&#xff1a;結構體的介紹 在之前的博客中&#xff0c;我們簡單介紹過了關于結構體的基本知識&#xff0c;這里我們稍微復習一下。 結構體(struct)是C語言中一種重要的復合數據類型&#xff0c;它允許將不同類型的數據組合成一個整體。 1.1結構體的定義 結構體使…

C++學習:六個月從基礎到就業——內存管理:new/delete操作符

C學習&#xff1a;六個月從基礎到就業——內存管理&#xff1a;new/delete操作符 本文是我C學習之旅系列的第十七篇技術文章&#xff0c;也是第二階段"C進階特性"的第二篇&#xff0c;主要介紹C中動態內存管理的核心操作符——new和delete。查看完整系列目錄了解更多…

15~30K,3年以上golang開發經驗

繼續分享最新的面經&#xff0c;前面發的兩篇大家也可以看看&#xff1a; 「坐標上海&#xff0c;20K的面試強度」「北京七貓&#xff0c;薪資25~35K&#xff0c;瞧瞧面試強度」 今天分享的是golang開發崗面經&#xff0c;要求是3年以上golang開發經驗&#xff0c;薪資為15~3…

Python爬蟲實戰:獲取優志愿專業數據

一、引言 在信息爆炸的當下,數據成為推動各領域發展的關鍵因素。優志愿網站匯聚了豐富的專業數據,對于教育研究、職業規劃等領域具有重要價值。然而,為保護自身數據和資源,許多網站設置了各類反爬機制。因此,如何高效、穩定地從優志愿網站獲取計算機專業數據成為一個具有…

ArcPy工具箱制作(下)

在上一篇博客中&#xff0c;我們已經初步了解了如何制作ArcPy工具箱&#xff0c;包括工具箱的基本概念、準備工作、腳本編寫以及將腳本轉換為工具箱的步驟。今天&#xff0c;我們將繼續深入探討ArcPy工具箱的制作&#xff0c;重點介紹一些進階技巧和優化方法. 一、優化工具箱的…

不一樣的flag 1(迷宮題)

題目 做法 下載壓縮包&#xff0c;解壓&#xff0c;把解壓后的文件拖進Exeinfo PE進行分析 32位&#xff0c;無殼 扔進IDA&#xff08;32位&#xff09;&#xff0c;找到main&#xff0c;F5反編譯 沒啥關鍵詞&#xff0c;ShiftF12也找不到什么有用的點 從上往下分析吧 puts(…

工程化實踐:Flutter項目結構與規范

工程化實踐&#xff1a;Flutter項目結構與規范 在Flutter項目開發中&#xff0c;良好的工程化實踐對于提高開發效率、保證代碼質量和團隊協作至關重要。本文將從項目結構、代碼規范、CI/CD流程搭建以及包管理等方面&#xff0c;詳細介紹Flutter項目的工程化最佳實踐。 項目結…

[Java · 初窺門徑] Java 語言初識

&#x1f31f; 想系統化學習 Java 編程&#xff1f;看看這個&#xff1a;[編程基礎] Java 學習手冊 0x01&#xff1a;Java 編程語言簡介 Java 是一種高級計算機編程語言&#xff0c;它是由 Sun Microsystems 公司&#xff08;已被 Oracle 公司收購&#xff09;于 1995 年 5 …

1187. 【動態規劃】競賽總分

題目描述 學生在我們USACO的競賽中的得分越多我們越高興。我們試著設計我們的競賽以便人們能盡可能的多得分。 現在要進行一次競賽&#xff0c;總時間T固定&#xff0c;有若干類型可選擇的題目&#xff0c;每種類型題目可選入的數量不限&#xff0c;每種類型題目有一個si(解答…

使用KeilAssistant代替keil的UI界面

目錄 一、keil Assistant的優勢和缺點 二、使用方法 &#xff08;1&#xff09;配置keil的路徑 &#xff08;2&#xff09;導入并使用工程 &#xff08;3&#xff09;默認使用keil自帶的ARM編譯器而非GUN工具鏈 一、keil Assistant的優勢和缺點 在日常學…

【React】通過 fetch 發起請求,設置 proxy 處理跨域

fetch 基本使用跨域處理 fetch 基本使用 在node使用原生ajax發請求&#xff1a;XMLHttpRequest()1.獲取xhr對象 2.注冊回調函數 3.設置參數&#xff0c;請求頭 4.發起連接原生ajax沒有帶異步處理 promise&#xff1b;原生ajax封裝一下&#xff0c;以便重復調用jQuery&#…

Redis(二) - Redis命令詳解

文章目錄 前言一、啟動Redis并進入客戶端1. 啟動Redis2. 進入Redis客戶端3. 使用IDEA連接Redis 二、查看命令幫助信息1. 查看所有命令2. 查看指定命令幫助 三、鍵操作命令1. set命令2. mset命令3. keys命令4. get命令5. mget命令6. dump命令7. exists命令8. type命令9. rename命…

【Qt】初識Qt(二)

目錄 一、顯示hello world1.1 圖形化界面1.2 寫代碼 二、對象樹三、使用輸入框顯示hello world四、使用按鈕顯示hello world 一、顯示hello world 有兩種方式實現hello world&#xff1a; 通過圖形化界面&#xff0c;在界面上創建出一個控件&#xff0c;顯示hello world通過寫…

空調制冷量和功率有什么關系?

空調的制冷量和功率是衡量空調性能的兩個核心參數,二者既有區別又緊密相關,以下是具體解析: 1. 基本定義 制冷量(Cooling Capacity)指空調在單位時間內從室內環境中移除的熱量,單位為 瓦特(W) 或 千卡/小時(kcal/h)。它直接反映空調的制冷能力,數值越大,制冷效果越…