換個姿勢,帶着問題看Handler
Handler,老生常談,網上關於它的文章也是“氾濫成災”,但實際開發很少手寫Handler,
畢竟,寫異步,RxAndroid鏈式調用 或者 Kotlin協程同步方式寫異步代碼 還是挺香的。
不過,面試官都喜歡章口就來一句:
當然,應對方法也很簡單,找一篇《…Handler詳解》之類的文章,背熟即可~
不過,對於我這種好刨根問底的人來說,自己過一遍源碼心理才踏實,
而且,我發現「帶着問題」看源碼,思考理解本質,印象更深,收穫更多,遂有此文。
羅列下本文提及的問題,如有答不出的可按需閱讀,謝謝~
- 1、Handler問題三連:是什麼?有什麼用?爲什麼要用Handler,不用行不行?
- 2、Android UI更新機制(GUI) 爲何設計成了單線程的?
- 3、真的只能在主(UI)線程中更新UI嗎?
- 4、真的不能在主(UI)線程中執行網絡操作嗎?
- 5、Handler怎麼用?
- 6、爲什麼建議使用Message.obtain()來創建Message實例?
- 7、爲什麼子線程中不可以直接new Handler()而主線程中可以?
- 8、主線程給子線程的Handler發送消息怎麼寫?
- 9、HandlerThread實現的核心原理?
- 10、當你用Handler發送一個Message,發生了什麼?
- 11、Looper是怎麼揀隊列裏的消息的?
- 12、分發給Handler的消息是怎麼處理的?
- 13、IdleHandler是什麼?
- 14、Looper在主線程中死循環,爲啥不會ANR?
- 15、Handler泄露的原因及正確寫法
- 16、Handler中的同步屏障機制
- 17、Android 11 Handler相關變更
0x1、Handler問題三連
1.Handler是什麼
答:Android定義的一套 子線程與主線程間通訊 的 消息傳遞機制 。
2.Handler有什麼用
答:把子線程中的 UI更新信息傳遞 給主線程(UI線程),以此完成UI更新操作。
3.爲什麼要用Handler,不用行不行
答:不行,Handler是android在設計之初就封裝了一套消息創建、傳遞、處理機制。
Android要求:
我們在主線程(UI)線程中更新UI
是要求,建議,不是規定,你不聽,硬要:
在子線程中更新UI,也是可以的!!!
比如,在一個子線程中,創建一個對話框:
運行後:
沒有報錯,對話框正常彈出,而我們平時在 子線程中更新UI 的錯:
異常翻譯:只有創建這個view的線程才能操作這個view;
引起原因:在子線程中更新了主線程創建的UI;
也就是說:子線程更新UI也行,但是隻能更新自己創建的View;
換句話說:Android的UI更新(GUI)被設計成了單線程;
你可能會問,爲啥不設計成多線程?
答:多個線程同時對同一個UI控件進行更新,容易發生不可控的錯誤!
那麼怎麼解決這種線程安全問題?
答:最簡單的 加鎖,不是加一個,是每層都要加鎖(用戶代碼→GUI頂層→GUI底層…)這樣也意味着更多的 耗時,UI更新效率降低;如果每層共用同一把鎖的話,其實就是單線程。
所以,結論是:
Android沒有采用「線程鎖」,而是採用「單線程消息隊列機制」,實現了一個「僞鎖」
這個疑問解決了,再說一個網上很常見的主線程更新UI的例子:
上面這段代碼 直接在子線程中更新了UI,卻沒有報錯:
這是要打臉嗎?但如果在子線程中加句線程休眠模擬耗時操作的話:
程序就崩潰了,報錯如下:
前面說了 Android的UI更新被設計成單線程,這裏妥妥滴會報錯,但卻發生在延遲執行後?
限於篇幅,這裏就不去跟源碼了,直接說原因:
ViewRootImp 在 onCreate() 時還沒創建;
在 onResume()時,即ActivityThread 的 handleResumeActivity() 執行後才創建,
調用 requestLayout(),走到 checkThread() 時就報錯了。
可以打個日誌簡單的驗證下:
加上休眠:
行吧,以後去面試別人問「子線程是不是一定不可以更新UI」別傻乎乎的說是了。
4.引生的另一個問題
說到「只能在主線程中更新UI」我又想到另一個問題「不能在主線程中進行網絡操作」
上述代碼運行直接閃退,日誌如下:
NetworkOnMainThreadException:網絡請求在主線程進行異常。
em… 真的不能在主線程中做網絡操作嗎?
在 onCreate() 的 setContentView() 後插入下面兩句代碼:
運行下看看:
這…又打臉?先說下 StrictMode(嚴苟模式)
Android 2.3 引入,用於檢測兩大問題:ThreadPolicy(線程策略) 和 VmPolicy(VM策略)
相關方法如下:
把嚴苟模式的網絡檢測關了,就可以 在主線程中執行網絡操作了,不過一般是不建議這樣做的:
在主線程中進行耗時操作,可能會導致程序無響應,即 ANR (Application Not Responding)。
至於常見的ANR時間,可以在對應的源碼中找到:
// ActiveServices.java → Service服務
static final int SERVICE_TIMEOUT = 20*1000; // 前臺
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 後臺
// ActivityManagerService.java → Broadcast廣播、InputDispatching、ContentProvider
static final int BROADCAST_FG_TIMEOUT = 10*1000; // 前臺
static final int BROADCAST_BG_TIMEOUT = 60*1000; // 後臺
static final int KEY_DISPATCHING_TIMEOUT = 5*1000; // 關鍵調度
static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000; // 內容提供者
時間統計區間:
- 起點:System_Server 進程調用 startProcessLocked 後調用 AMS.attachApplicationLocked()
- 終點:Provider 進程 installProvider及publishContentProviders 調用到 AMS.publishContentProviders()
- 超過這個時間,系統就會殺掉 Provider 進程。
0x2、Handler怎麼用
1.sendMessage() + handleMessage()
代碼示例如下:
黃色部分會有如下警告
Handler不是靜態類可能引起「內存泄露」,原因以及正確寫法等下再講。
另外,建議調用 Message.obtain() 函數來獲取一個Message實例,爲啥?點進源碼:
從源碼,可以看到obtain()的邏輯:
- 加鎖,判斷Message池是否爲空
- ① 不爲空,取一枚Message對象,正在使用標記置爲0,池容量-1,返回;
- ② 爲空,新建一個Message對象,返回;
Message池複用Message,可以「避免重複創建實例對象」節約內存,
另外,Message池其實是一個「單鏈表結構」
上述獲取消息池的邏輯:
定位到下述代碼,還可以知道:池的容量爲50,超過
然後問題來了,Message信息什麼時候加到池中?
答:當Message 被Looper分發完後,會調用 recycleUnchecked()函數,回收沒有在使用的Message對象。
標誌設置爲FLAG_IN_USE,表示正在使用,相關屬性重置,加鎖,判斷消息池是否滿,
未滿,「單鏈表頭插法」把消息插入到表頭。(獲取和插入都發生在表頭,像不像 棧~)
2.post(runnable)
代碼示例如下:
跟下post():
實際上調用了 sendMessageDelayed() 發送消息,只不過延遲秒數爲0,
那Runnable是怎麼變成Message的呢?跟下getPostMessage()
噢,獲取一個新的Message示例後,把 Runnable 變量的值賦值給 callback屬性
3.附:其他兩個種在子線程中更新UI的方法
activity.runOnUiThread()
view.post() 與 view.postDelay()
0x3、Handler底層原理解析
終於來到稍微有點技術含量的環節,在觀摩源碼瞭解原理前,先說下幾個涉及到的類。
1.涉及到的幾個類
2.前戲
在我們使用Handler前,Android系統已爲我們做了一系列的工作,其中就包括了
創建「Looper」和「MessageQueue」對象
上圖中有寫:ActivityThread 的 main函數是APP進程的入口,定位到 ActivityThread → main函數
定位到:Looper → prepareMainLooper函數
定位到:Looper → prepare函數
定位到:Looper → Looper構造函數
另外這裏的 mQuitAllowed 變量,直譯「退出允許」,具體作用是?跟下 MessageQueue:
em…用來 防止開發者手動終止消息隊列,停止Looper循環。
3.消息隊列的運行
前戲過後,創建了Looper與MessageQueue對象,接着調用Looper.loop()開啓輪詢。
定位到:Looper → loop函數
接着有幾個問題,先是這個 myLooper() 函數:
這裏的 ThreadLocal → 線程局部變量 → JDK提供的用於解決線程安全的工具類。
作用:爲每個線程提供一個獨立的變量副本 → 以解決併發訪問的衝突問題。
本質:
每個Thread內部都維護了一個ThreadLocalMap,這個map的key是ThreadLocal,
value是set的那個值。get的時候,都是從自己的變量中取值,所以不存在線程安全問題。
主線程和子線程的Looper對象實例相互隔離的!!!
意味着:主線程和子線程Looper不是同一個!!!
知道這個以後,有個問題就解惑了:
「爲什麼子線程中不能直接 new Handler(),而主線程可以?」
答:主線程與子線程不共享同一個Looper實例,主線程的Looper在啓動時就通過
prepareMainLooper() 完成了初始化,而子線程還需要調用 Looper.prepare()
和 Looper.loop()開啓輪詢,否則會報錯,不信,可以試試:
直接就奔潰了~
加上試試?
可以,程序正常運行,沒有報錯。
對了,既然說Handler用於子線程和主線程通信,試試在主線程中給子線程的Handler發送信息,修改一波代碼:
運行,直接報錯:
原因:多線程併發的問題,當主線程執行到sendEnptyMessage時,子線程的Handler還沒有創建。
一個簡單的解決方法是:主線程延時給子線程發消息,修改後的代碼示例如下:
運行結果如下:
可以,不過其實Android已經給我們封裝好了一個輕量級的異步類「HandlerThread」
4.HandlerThread
HandlerThread = 繼承Thread + 封裝Looper
使用方法很簡單,改造下我們上面的代碼:
用法挺簡單的,源碼其實也很簡單,跟一跟:
剩下一個quit()和quitSafely()停止線程,就不用說了,所以HandlerThread的核心原理就是:
- 繼承Thread,getLooper()加鎖死循環wait()堵塞線程;
- run()加鎖等待Looper對象創建成功,notifyAll()喚醒線程;
- 喚醒後,getLooper返回由run()中生成的Looper對象;
是吧,HandlerThread的實現原理竟簡單如斯,另外,順帶提個醒!!!
Java中所有類的父類是 Object 類,裏面提供了wait、notify、notifyAll三個方法;
Kotlin 中所有類的父類是 Any 類,裏面可沒有上述三個方法!!!
所以你不能在kotlin類中直接調用,但你可以創建一個java.lang.Object的實例作爲lock,
去調用相關的方法。
代碼示例如下:
private val lock = java.lang.Object()
fun produce() = synchronized(lock) {
while(items>=maxItems) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items++
println("Produced, count is$items:${Thread.currentThread()}")
lock.notifyAll()
}
fun consume() = synchronized(lock) {
while(items<=0) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items--
println("Consumed, count is$items:${Thread.currentThread()}")
lock.notifyAll()
}
5.當我們用Handler發送一個消息發生了什麼?
扯得有點遠了,拉回來,剛講到 ActivityThread 在 main函數中調用 Looper.prepareMainLooper
完成主線程 Looper初始化,然後調用 Looper.loop() 開啓消息循環 等待接收消息。
嗯,接着說下 發送消息,上面說了,Handler可以通過sendMessage()和 post() 發送消息,
上面也說了,源碼中,這兩個最後調用的其實都是 sendMessageDelayed()完成的:
第二個參數:當前系統時間+延時時間,這個會影響「調度順序」,跟 sendMessageAtTime()
獲取當前線程Looper中的MessageQueue隊列,判空,空打印異常,否則返回 enqueueMessage(),跟:
這裏的 mAsynchronous 是 異步消息的標誌,如果Handler構造方法不傳入這個參數,默認false:
這裏涉及到了一個「同步屏障」的東西,等等再講,跟:MessageQueue -> enqueueMessage
如果你瞭解數據結構中的單鏈表的話,這些都很簡單。
不瞭解的可以移步至【面試】數據結構與算法(二) 學習一波~
6.Looper是怎麼揀隊列的消息的?
MessageQueue裏有Message了,接着就該由Looper來分揀了,定位到:Looper → loop函數
// Looper.loop()
final Looper me = myLooper(); // 獲得當前線程的Looper實例
final MessageQueue queue = me.mQueue; // 獲取消息隊列
for (;;) { // 死循環
Message msg = queue.next(); // 取出隊列中的消息
msg.target.dispatchMessage(msg); // 將消息分發給Handler
}
queue.next() 從隊列拿出消息,定位到:MessageQueue -> next函數:
這裏的關鍵其實就是:nextPollTimeoutMillis,決定了堵塞與否,以及堵塞的時間,三種情況:
等於0時,不堵塞,立即返回,Looper第一次處理消息,有一個消息處理完 ;
大於0時,最長堵塞等待時間,期間有新消息進來,可能會了立即返回(立即執行);
等於-1時,無消息時,會一直堵塞;
Tips:此處用到了Linux的pipe/epoll機制:沒有消息時阻塞線程並進入休眠釋放cpu資源,有消息時喚醒線程;
7.分發給Handler的消息是怎麼處理的?
通過MessageQueue的queue.next()揀出消息後,調用msg.target.dispatchMessage(msg)
把消息分發給對應的Handler,跟到:Handler -> dispatchMessage
到此,關於Handler的基本原理也說的七七八八了~
8.IdleHandler是什麼?
評論區有小夥子說:把idleHandler加上就完整了,那就安排下吧~
在 MessageQueue 類中有一個 static 的接口 IdleHanlder
翻譯下注釋:當線程將要進入堵塞,以等待更多消息時,會回調這個接口;
簡單點說:當MessageQueue中無可處理的Message時回調;
作用:UI線程處理完所有View事務後,回調一些額外的操作,且不會堵塞主進程;
接口中只有一個 queueIdle() 函數,線程進入堵塞時執行的額外操作可以寫這裏,
返回值是true的話,執行完此方法後還會保留這個IdleHandler,否則刪除。
使用方法也很簡單,代碼示例如下:
輸出結果如下:
看下源碼,瞭解下具體的原理:MessageQueue,定義了一個IdleHandler的列表和數組
定義了添加和刪除IdleHandler的函數:
在 next() 函數中用到了 mIdleHandlers 列表:
原理就這樣,一般使用場景:繪製完成回調,例子可參見:
《你知道 android 的 MessageQueue.IdleHandler 嗎?》
也可以在一些開源項目上看到IdleHandler的應用:
http://useof.org/java-open-source/android.os.MessageQueue.IdleHandler
0x4、一些其他問題
1.Looper在主線程中死循環,爲啥不會ANR?
答:上面說了,Looper通過queue.next()獲取消息隊列消息,當隊列爲空,會堵塞,
此時主線程也堵塞在這裏,好處是:main函數無法退出,APP不會一啓動就結束!
你可能會問:主線程都堵住了,怎麼響應用戶操作和回調Activity聲明週期相關的方法?
答:application啓動時,可不止一個main線程,還有其他兩個Binder線程:ApplicationThread 和 ActivityManagerProxy,用來和系統進程進行通信操作,接收系統進程發送的通知。
- 當系統受到因用戶操作產生的通知時,會通過 Binder 方式跨進程通知 ApplicationThread;
- 它通過Handler機制,往 ActivityThread 的 MessageQueue 中插入消息,喚醒了主線程;
- queue.next() 能拿到消息了,然後 dispatchMessage 完成事件分發;
PS:ActivityThread 中的內部類H中有具體實現
死循環不會ANR,但是 dispatchMessage 中又可能會ANR哦!如果你在此執行一些耗時操作,
導致這個消息一直沒處理完,後面又接收到了很多消息,堆積太多,就會引起ANR異常!!!
2.Handler泄露的原因及正確寫法
上面說了,如果直接在Activity中初始化一個Handler對象,會報如下錯誤:
原因是:
在Java中,非靜態內部類會持有一個外部類的隱式引用,可能會造成外部類無法被GC;
比如這裏的Handler,就是非靜態內部類,它會持有Activity的引用從而導致Activity無法正常釋放。
而單單使用靜態內部類,Handler就不能調用Activity裏的非靜態方法了,所以加上「弱引用」持有外部Activity。
代碼示例如下:
private static class MyHandler extends Handler {
//創建一個弱引用持有外部類的對象
private final WeakReference<MainActivity> content;
private MyHandler(MainActivity content) {
this.content = new WeakReference<MainActivity>(content);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity= content.get();
if (activity != null) {
switch (msg.what) {
case 0: {
activity.notifyUI();
}
}
}
}
}
轉換成Kotlin:(Tips:Kotlin 中的內部類,默認是靜態內部類,使用inner修飾才爲非靜態~)
private class MyHandler(content: MainActivity) : Handler() {
//創建一個弱引用持有外部類的對象
private val content: WeakReference<MainActivity> = WeakReference(content)
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val activity = content.get()
if (activity != null) {
when (msg.what) {
0 -> {
activity.notifyUI()
}
}
}
}
}
3.同步屏障機制
通過上面的學習,我們知道用Handler發送的Message後,MessageQueue的enqueueMessage()按照 時間戳升序 將消息插入到隊列中,而Looper則按照順序,每次取出一枚Message進行分發,一個處理完到下一個。這時候,問題來了:有一個緊急的Message需要優先處理怎麼破?你可能或說**直接sendMessage()**不就可以了,不用等待立馬執行,看上去說得過去,不過可能有這樣一個情況:
一個Message分發給Handler後,執行了耗時操作,後面一堆本該到點執行的Message在那裏等着,這個時候你sendMessage(),還是得排在這堆Message後,等他們執行完,再到你!
對吧?Handler中加入了「同步屏障」這種機制,來實現「異步消息優先執行」的功能。
添加一個異步消息的方法很簡單:
- 1、Handler構造方法中傳入async參數,設置爲true,使用此Handler添加的Message都是異步的;
- 2、創建Message對象時,直接調用setAsynchronous(true)
一般情況下:同步消息和異步消息沒太大差別,但僅限於開啓同步屏障之前。可以通過 MessageQueue 的 postSyncBarrier 函數來開啓同步屏障:
行吧,這一步簡單的說就是:往消息隊列合適的位置插入了同步屏障類型的Message (target屬性爲null)
接着,在 MessageQueue 執行到 next() 函數時:
遇到target爲null的Message,說明是同步屏障,循環遍歷找出一條異步消息,然後處理。
在同步屏障沒移除前,只會處理異步消息,處理完所有的異步消息後,就會處於堵塞。
如果想恢復處理同步消息,需要調用 removeSyncBarrier() 移除同步屏障:
在API 28的版本中,postSyncBarrier()已被標註hide,但依舊可在系統源碼中找到相關應用,比如:
爲了更快地響應UI刷新事件,在ViewRootImpl的scheduleTraversals函數中就用到了同步屏障:
4、Android 11 R Handler 變更
官方文檔:https://developer.android.google.cn/reference/android/os/Handle
構造函數:
- Handler() 廢棄 → Handler(Looper.myLooper())
- Handler(Handler.Callback callback) 廢棄 → Handler(Looper.myLooper(), callback)
Looper.prepareMainLooper () 廢棄
原因:主線程的Looper是由系統自動創建的,無需用戶自行調用。
參考文獻:
- Android Handler:手把手帶你深入分析 Handler機制源碼
- Handler 都沒搞懂,拿什麼去跳槽啊?
- Android中爲什麼主線程不會因爲Looper.loop()裏的死循環卡死?
- GUI爲什麼不設計爲多線程
- Android 11 ® 之 Handler 相關變化