進階之路 | 奇妙的Handler之旅
- 2020 年 3 月 6 日
- 筆記
前言
本文已經收錄到我的Github個人部落格,歡迎大佬們光臨寒舍:
需要已經具備的知識:
Handler
的基本概念及使用
學習導圖:
一.為什麼要學習Handler
?
在Android
平台上,主要用到的通訊機制有兩種:Handler
和Binder
,前者用於進程內部的通訊,後者主要用於跨進程通訊。
在多執行緒的應用場景中,Handler
將工作執行緒中需更新UI
的操作資訊 傳遞到 UI
主執行緒,從而實現工作執行緒對UI
的更新處理,最終實現非同步消息的處理。
作為一個Android
程式猿,知其然而必須知其所以然,理解其源碼能更好地了解Handler
機制的原理。下面,我就從消息機制入手,帶大家暢遊在Handler
的世界中,體會Google
工程師的智慧之光。
二.核心知識點歸納
2.1 消息機制概述
A.作用:跨執行緒通訊
B.常用場景:當子執行緒中進行耗時操作後需要更新UI
時,通過Handler
將有關UI
的操作切換到主執行緒中執行
系統不建議在子執行緒訪問
UI
的原因:UI
控制項非執行緒安全,在多執行緒中並發訪問可能會導致UI
控制項處於不可預期的狀態而不對
UI
控制項的訪問加上鎖機制的原因有:1.上鎖會讓UI控制項變得複雜和低效
2.上鎖後會阻塞某些進程的執行
C.四要素:
Message
:需要被傳遞的消息,其中包含了消息ID
,消息處理對象以及處理的數據等,由MessageQueue
統一列隊,最終由Handler
處理MessageQueue
:用來存放Handler
發送過來的消息,內部通過單鏈表的數據結構來維護消息列表,等待Looper
的抽取。Handler
:負責Message
的發送及處理
Handler.sendMessage()
:向消息隊列發送各種消息事件Handler.handleMessage()
:處理相應的消息事件
Looper
:通過Looper.loop()
不斷地從MessageQueue
中抽取Message
,按分發機制將消息分發給目標處理者,可以看成是消息泵
Thread
:負責調度整個消息循環,即消息循環的執行場所存在關係:
- 一個
Thread
只能有一個Looper
,可以有多個Handler
Looper
有一個MessageQueue
,可以處理來自多個Handler
的Message
MessageQueue
有一組待處理的Message
,這些Message
可來自不同的Handler
Message
中記錄了負責發送和處理消息的Handler
Handler
中有Looper
和MessageQueue
D.使用方法:
- 在
ActivityThread
主執行緒實例化一個全局的Handler
對象 - 在需要執行
UI
操作的子執行緒里實例化一個Message
並填充必要數據,調用Handler.sendMessage(Message)
方法發送出去 - 重寫
handleMessage()
方法,對不同Message
執行相關操作
E.總體工作流程:
這裡先總體地說明一下
Android
消息機制的工作流程,具體的ThreadLocal
,MessageQueue
,Looper
,Handler
的工作原理會在下文詳細解析
Handler.sendMessage()
發送消息時,會通過MessageQueue.enqueueMessage()
向MessageQueue
中添加一條消息- 通過
Looper.loop()
開啟循環後,不斷輪詢調用MessageQueue.next()
- 調用目標
Handler.dispatchMessage()
去傳遞消息,目標Handler
收到消息後調用Handler.handleMessage()
處理消息
簡單來看,即
Handler
將Message
發送到Looper
的成員變數MessageQueue
中,之後Looper
不斷循環遍歷MessageQueue
從中讀取Message
,最終回調給Handler
處理。如圖:
2.2 消息機制分析
2.2.1 ThreadLocal
了解
ThreadLocal
,有助於我們後面對Looper
的探究
Q1:ThreadLocal
是什麼
首先我們來看一下官方源碼(Android 9.0
)
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
大致意思:
ThreadLocal
是一個執行緒內部的數據存儲類,通過它可以在指定的執行緒中存儲數據,只有在指定執行緒中才能獲取到存儲的數據(也就是說,每個執行緒的一個變數,有自己的值)
Q2:ThreadLocal
的使用場景:
- 當某些數據是以執行緒為作用域且每個執行緒有特有的數據副本
Android
中具體的使用場景:Looper
,ActivityThread
,AMS
如果不採用
ThreadLocal
的話,需要採取的措施:提供一個全局哈希表
- 複雜邏輯下的對象傳遞,比如:監聽器的傳遞
採用
ThreadLocal
讓監聽器作為執行緒中的全局對象,執行緒內部只有通過get
方法即可得到監聽器如果不採用
ThreadLocal
的方案:a.將監聽器作為參數傳遞
缺點:當調用棧很深的時候,程式設計看起來不美觀
b.將監聽器作為靜態變數
缺點:狀態不具有可擴充性
Q3:ThreadLocal
和synchronized
的區別:
- 對於多執行緒資源共享的問題,
synchronized
機制採用了「以時間換空間」的方式- 而
ThreadLocal
採用了「以空間換時間」的方式- 前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響,所以
ThreadLocal
和synchronized
都能保證執行緒安全,但是應用場景卻大不一樣。
Q4:原理
ThreadLocal
主要操作為set
,get
操作,下面分別介紹流程
A1:set
的原理
A2:get
的原理
綜上所述,ThreadLocal
之所以有這麼奇妙的效果,是因為:
- 不同執行緒訪問同一個
ThreadLocal.get()
,其內部會從各種執行緒中取出table
數組,然後根據當前ThreadLocal
的索引查找出對應的values
值
想要了解
ThreadLocal
源碼的讀者,推薦一篇文章:ThreadLocal詳解
2.2.2 MessageQueue
-
數據結構:
MessageQueue
的數據結構是單鏈表 -
操作:
A.
enqueueMessage
主要操作是單鏈表的插入操作
B.
next
是一個無限循環的方法,如果沒有消息,會一直阻塞;當有消息的時候,
next
會返回消息並將其從單鏈表中移出
2.2.3 Looper
Q1:Looper
的作用
- 作為消息循環的角色
- 它會不停地從
MessageQueue
中查看是否有新消息,若有新消息則立即處理,否則一直阻塞(不是ANR
)Handler
需要Looper
,否則將報錯
Q2:Looper
的使用
a1:開啟:
UI
執行緒會自動創建Looper
,子執行緒需自行創建
//子執行緒中需要自己創建一個Looper new Thread(new Runnable() { @Override public void run() { Looper.prepare();//為子執行緒創建Looper Handler handler = new Handler(); Looper.loop(); //開啟消息輪詢 } }).start();
- 除了
prepare()
,還提供prepareMainLooper()
,本質也是通過prepare()
getMainLooper()
作用:獲取主執行緒的Looper
a2:關閉:
quit
:直接退出quitSafely
:設定退出標記,待MessageQueue
中處理完所有消息再退出
退出
Looper
的話,子執行緒會立刻終止;因此:建議在不需要的時候終止Looper
Q3:原理:
2.2.4 Handler
Q1:Handler
的兩種使用方式:
注意:創建
Handler
實例之前必須先創建Looper
實例,否則會拋RuntimeException
(UI
執行緒自動創建Looper
)
send
方式
//第一種:send方式的Handler創建 Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { //如UI操作 } }; //send mHandler.sendEmptyMessage(0);
post
方式
最終是通過一系列
send
方法來實現
//實例化Handler private Handler mHandler = new Handler(); //這裡調用了post方法,和sendMessage一樣達到了更新UI的目的 mHandler.post(new Runnable() { @Override public void run() { mTextView.setText(new_str); } });
Q2:Handler
處理消息過程
2.3 Handler
的延伸
2.3.1 記憶體泄露
在初學
Handler
的時候,往往會發現AS
亮起一大塊黃色,以警告可能會發生記憶體泄漏
-
發生場景:
Handler
允許我們發送延時消息,如果在延時期間用戶關閉了Activity
,那麼該Activity
會泄露 -
原因:這個泄露是因為因為
Java
的特性,內部類會持有外部類,Handler
持有Activity
的引用,Message
持有Handler
的引用,而MessageQueue
會持有Message
的引用,而MessageQueue
是屬於TLS(ThreadLocalStorage)
執行緒,是與Activity不同的生命周期。所以當Activity
的生命周期結束後,而MessageQueue
中還存在未處理的消息,那麼上面一連串的引用鏈就不允許Activity
的對象被回收,就造成了記憶體泄漏 -
解決方式:
A.
Activity
銷毀時,清空Handler
中未執行或正在執行的Callback
以及Message
// 清空消息隊列,移除對外部類的引用 @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
B.靜態內部類+弱引用
private static class AppHandler extends Handler { //弱引用,在垃圾回收時,被回收 WeakReference<Activity> mActivityReference; AppHandler(Activity activity){ mActivityReference=new WeakReference<Activity>(activity); } public void handleMessage(Message message){ switch (message.what){ HandlerActivity activity=mActivityReference.get(); super.handleMessage(message); if(activity!=null){ //執行業務邏輯 } } } }
2.3.2 Handler
里藏著的Callback
首先看下Handler.dispatchMessage(msg)
public void dispatchMessage(Message msg) { //這裡的 callback 是 Runnable if (msg.callback != null) { handleCallback(msg); } else { //如果 callback 處理了該 msg 並且返回 true, 就不會再回調 handleMessage if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
可以看到 Handler.Callback
有優先處理消息的權利
- 當一條消息被
Callback
處理並攔截(返回true
),那麼Handler.handleMessage(Msg)
方法就不會被調用了 - 如果
Callback
處理了消息,但是並沒有攔截,那麼就意味著一個消息可以同時被Callback
以及Handler
處理
這個就很有意思了,這有什麼作用呢?
我們可以利用 Callback
這個攔截機制來攔截 Handler
的消息!
場景:Hook
ActivityThread.mH ,筆者在進階之路 | 奇妙的四大組件之旅介紹過ActivityThread
,在 ActivityThread
中有個成員變數 mH
,它是個 Handler
,又是個極其重要的類,幾乎所有的插件化框架都使用了這個方法
限於當前知識水平,筆者尚未研究過插件化的知識,以後有機會的話希望能給大家介紹!
2.3.3 創建 Message
的最佳方式
為了節省開銷,盡量復用
Message
,減少記憶體消耗
法一:Message msg=Message.obtain();
法二:Message msg=handler.obtainMessage();
2.3.4 妙用 Looper
機制
我們可以利用Looper
的機制來幫助我們做一些事情:
- 將
Runnable
post
到主執行緒執行 - 利用
Looper
判斷當前執行緒是否是主執行緒
public final class MainThread { private MainThread() { } private static final Handler HANDLER = new Handler(Looper.getMainLooper()); //將 Runnable post 到主執行緒執行 public static void run(@NonNull Runnable runnable) { if (isMainThread()) { runnable.run(); }else{ HANDLER.post(runnable); } } //判斷當前執行緒是否是主執行緒 public static boolean isMainThread() { return Looper.myLooper() == Looper.getMainLooper(); } }
2.3.5 Android
中為什麼主執行緒不會因Looper.loop()
的死循環卡死?
這個是老生常談的問題了,記得當初被學長問到這個問題的時候,一臉懵逼,然後胡說一通,實屬羞愧
要弄清這個問題,我們可以通過幾個問題來逐層深入剖析
Q1:什麼是執行緒?
執行緒是一段可執行的程式碼,當可執行程式碼執行完成後,執行緒生命周期便該終止了,執行緒退出
Q2:進入死循環是不是說明一定會阻塞?
前面也說到了執行緒既然是一段可執行的程式碼,當可執行程式碼執行完成後,執行緒生命周期便該終止了,執行緒退出。而對於主執行緒,我們是絕不希望會被運行一段時間,自己就退出,那麼如何保證能一直存活呢?簡單做法就是可執行程式碼是能一直執行下去的,死循環便能保證不會被退出
想到這就理解,主執行緒也是一個執行緒,它也要維持自己的周期,所以也是需要一個死循環的。所以死循環並不是那麼讓人擔心。
Q3:什麼是Looper
的阻塞?
Looper
的阻塞,前提是沒有輸入事件,此時MessageQueue
是空的,Looper
進入空閑,執行緒進入阻塞,釋放CPU
,等待輸入事件的喚醒Looper
阻塞的時候,主執行緒大多數時候都是處於休眠狀態,並不會消耗大量CPU
資源
Looper
的阻塞涉及到Linux pipe/epoll
機制,想了解的讀者可自行
Q4:聊聊ANR
- 其實初學者很容易將
ANR
和Looper的阻塞
二者相混淆 UI
耗時導致卡死,前提是要有輸入事件,此時MessageQueue
不是空的,Looper
正常輪詢,執行緒並沒有阻塞,但是該事件執行時間過長(一般5秒),而且與此期間其他的事件(按鍵按下,螢幕點擊..也是通過Looper
處理的)都沒辦法處理(卡死),然後就ANR
異常了
Q5:卡死的真正原因:
- 真正卡死的原因是:在回調方法
onCreate
/onStart
/onResume
等操作時間過長
三.課堂小測試
恭喜你!已經看完了前面的文章,相信你對
Handler
已經有一定深度的了解,下面,進行一下課堂小測試,驗證一下自己的學習成果吧!PS:限於篇幅,筆者就不提供答案了,不過答案一搜就有了
Q1:如何將一個Thread
執行緒變成Looper
執行緒?Looper
執行緒有哪些特點
Q2:簡述下Handler
、Message
、Looper
的作用,以及他們之間的關係
Q3: 簡述消息機制的回調處理過程,怎麼保證消息處理機制的唯一性
Q4:為什麼發送消息在子執行緒,而處理消息就變成主執行緒了,在哪兒跳轉的
如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力
本文參考鏈接:
- 《Android 開發藝術探索》
- ThreadLocal詳解
- 進階之路 | 奇妙的四大組件之旅
- Handler運行機制中必須明白的幾個問題
- Handler 都沒搞懂,拿什麼去跳槽啊?
- Android中為什麼主執行緒不會因為Looper.loop()里的死循環卡死?
- 為什麼主執行緒不會因為Looper.loop()方法造成阻塞
- 要點提煉|開發藝術之消息機制
- Android消息機制淺析——面試總結
- Handler的sendMessage和post的區別