Handler,老生常談,網上關於它的文章可謂是「氾濫成災」,不過實際開發中用得很少。
畢竟,如今寫異步,RxAndroid鏈式調用、Kotlin協程同步方式寫異步代碼,不香麼?
不過,面試官仍是喜歡章口就萊一句:java
固然,應對方法也很簡單,找一篇《…Handler詳解》之類的文章,背熟便可~
不過,對於我這種好刨根問底的人來講,本身過一遍源碼心理才踏實,
並且,我發現「帶着問題」看源碼,思考理解本質,印象更深,收穫更多,遂有此文。android
羅列下本文說起的問題,若有答不出的可按需閱讀本文,謝謝~web
- 一、Handler問題三連:是什麼?有什麼用?爲何要用Handler,不用行不行?
- 二、真的只能在主(UI)線程中更新UI嗎?
- 三、真的不能在主(UI)線程中執行網絡操做嗎?
- 四、Handler怎麼用?
- 五、爲何建議使用Message.obtain()來建立Message實例?
- 六、爲何子線程中不能夠直接new Handler()而主線程中能夠?
- 七、主線程給子線程的Handler發送消息怎麼寫?
- 八、HandlerThread實現的核心原理?
- 九、當你用Handler發送一個Message,發生了什麼?
- 十、Looper是怎麼揀隊列裏的消息的?
- 十一、分發給Handler的消息是怎麼處理的?
- 十二、IdleHandler是什麼?
- 1三、Looper在主線程中死循環,爲啥不會ANR?
- 1四、Handler泄露的緣由及正確寫法
- 1五、Handler中的同步屏障機制
答:Android定義的一套 子線程與主線程間通信 的 消息傳遞機制 。面試
答:把子線程中的 UI更新信息,傳遞 給主線程(UI線程),以此完成UI更新操做。算法
答:不行,由於android在設計之初就封裝了一套消息建立、傳遞、處理。若是不遵循就不能更新UI信息,就會報出異常(異步消息處理異常)數組
在Android中,爲了提升系統運行效率,沒有采用「線程鎖」,帶來了:安全
多個線程併發更新UI時的線程安全問題網絡
爲了安全保證UI操做是線程安全的,規定數據結構
只能在主線程(UI線程)中完成UI更新多線程
但,真的只能在UI線程中更新UI嗎?
上面這段代碼 直接在子線程中更新了UI,卻沒有報錯:
這是要打臉嗎?但若是在子線程中加句線程休眠模擬耗時操做的話:
程序就崩潰了,報錯以下:
翻譯一下異常信息:只有建立這個view的線程才能操做這個view。限於篇幅,這裏就不去跟源碼了,直接說緣由:
ViewRootImp 在 onCreate() 時還沒建立;
在 onResume()時,即ActivityThread 的 handleResumeActivity() 執 行後才建立,
調用 requestLayout(),走到 checkThread() 時就報錯了。
能夠打個日誌簡單的驗證下:
加上休眠:
行吧,之後去面試別人問「子線程是否是必定不能夠更新UI」別傻乎乎的說是了。
說到「只能在主線程中更新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 進程。
代碼示例以下:
黃色部分會有以下警告
Handler不是靜態類可能引發「內存泄露」,緣由以及正確寫法等下再講。
另外,建議調用 Message.obtain() 函數來獲取一個Message實例,爲啥?點進源碼:
從源碼能夠看到obtain()的邏輯:
- 一、判斷Message池是否爲空;
- 二、不爲空,取出一個Message對象,池容量-1,返回;
- 三、不然,新建一個Message對象,返回;
這樣能夠「避免重複建立多個實例對象」節約內存,還有,Message池實際上是一個「單鏈表結構」,定位到下述代碼能夠看到:池的容量爲50
而後問題來了,Message信息何時加到池中?
當Message 被Looper分發完後,會調用 recycleUnchecked()函數,回收沒有在使用的message對象。
若是你懂點數據結構的話,能夠看出這是「單鏈表的頭插法」
代碼示例以下:
跟下post():
實際上調用了 sendMessageDelayed() 發送消息,只不過延遲秒數爲0,
那Runnable是怎麼變成Message的呢?跟下getPostMessage()
噢,獲取一個新的Message示例後,把 Runnable 變量的值賦值給 callback屬性
activity.runOnUiThread()
view.post() 與 view.postDelay()
終於來到稍微有點技術含量的環節,在觀摩源碼瞭解原理前,先說下幾個涉及到的類。
在咱們使用Handler前,Android系統已爲咱們作了一系列的工做,其中就包括了
建立「Looper」和「MessageQueue」對象
上圖中有寫:ActivityThread 的 main函數是APP進程的入口,定位到 ActivityThread → main函數
定位到:Looper → prepareMainLooper函數
定位到:Looper → prepare函數
定位到:Looper → Looper構造函數
另外這裏的 quitAllowed 變量,直譯「退出容許」,具體做用是?跟下 MessageQueue:
em…用來 防止開發者手動終止消息隊列,中止Looper循環。
前戲事後,建立了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」
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()
}
複製代碼
扯得有點遠了,拉回來,剛講到 ActivityThread 在 main函數中調用 Looper.prepareMainLooper
完成主線程 Looper初始化,而後調用 Looper.loop() 開啓消息循環 等待接收消息。
嗯,接着說下 發送消息,上面也說了,Handler能夠經過sendMessage()和 post() 發送消息,
上面也說了,源碼中,這兩個最後調用的其實都是 sendMessageDelayed()完成的:
第二個參數:當前系統時間+延時時間,這個會影響「調度順序」,跟 sendMessageAtTime()
獲取當前線程Looper中的MessageQueue隊列,判空,空打印異常,不然返回 enqueueMessage(),跟:
這裏的 mAsynchronous 是 異步消息的標誌,若是Handler構造方法不傳入這個參數,默認false:
這裏涉及到了一個「同步屏障」的東西,等等再講,跟:MessageQueue -> enqueueMessage
若是你瞭解數據結構中的單鏈表的話,這些都很簡單。
不瞭解的能夠移步至【面試】數據結構與算法(二) 學習一波~
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資源,有消息時喚醒線程;
經過MessageQueue的queue.next()揀出消息後,調用msg.target.dispatchMessage(msg)
把消息分發給對應的Handler,跟到:Handler -> dispatchMessage
到此,關於Handler的基本原理也說的七七八八了~
評論區有小夥子說:把idleHandler加上就完整了,那就安排下吧~
在 MessageQueue 類中有一個 static 的接口 IdleHanlder
翻譯下注釋:當線程將要進入堵塞,以等待更多消息時,會回調這個接口;
簡單點說:當MessageQueue中無可處理的Message時回調;
做用:UI線程處理完全部View事務後,回調一些額外的操做,且不會堵塞主進程;
接口中只有一個 queueIdle() 函數,線程進入堵塞時執行的額外操做能夠寫這裏,
返回值是true的話,執行完此方法後還會保留這個IdleHandler,不然刪除。
使用方法也很簡單,代碼示例以下:
輸出結果以下:
看下源碼,瞭解下具體的原理:MessageQueue,定義了一個IdleHandler的列表和數組
定義了添加和刪除IdleHandler的函數:
在 next() 函數中用到了 mIdleHandlers 列表:
原理就這樣,通常使用場景:繪製完成回調,例子可參見:
《你知道 android 的 MessageQueue.IdleHandler 嗎?》
也能夠在一些開源項目上看到IdleHandler的應用:
useof.org/java-open-s…
答:上面說了,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異常!!!
上面說了,若是直接在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()
}
}
}
}
}
複製代碼
經過上面的學習,咱們知道用Handler發送的Message後,MessageQueue的enqueueMessage()
按照 時間戳升序 將消息插入到隊列中,而Looper則按照順序,每次取出一枚Message進行分發,
一個處理完到下一個。這時候,問題來了:有一個緊急的Message須要優先處理怎麼破?
你可能或說直接sendMessage()不就能夠了,不用等待立馬執行,看上去說得過去,不過可能
有這樣一個狀況:
一個Message分發給Handler後,執行了耗時操做,後面一堆本該到點執行的Message在那裏等着,這個時候你sendMessage(),仍是得排在這堆Message後,等他們執行完,再到你!
對吧?Handler中加入了「同步屏障」這種機制,來實現「異步消息優先執行」的功能。
添加一個異步消息的方法很簡單:
- 一、Handler構造方法中傳入async參數,設置爲true,使用此Handler添加的Message都是異步的;
- 二、建立Message對象時,直接調用setAsynchronous(true)
通常狀況下:同步消息和異步消息沒太大差異,但僅限於開啓同步屏障以前。
能夠經過 MessageQueue 的 postSyncBarrier 函數來開啓同步屏障:
行吧,這一步簡單的說就是:往消息隊列合適的位置插入了同步屏障類型的Message (target屬性爲null)
接着,在 MessageQueue 執行到 next() 函數時:
遇到target爲null的Message,說明是同步屏障,循環遍歷找出一條異步消息,而後處理。
在同步屏障沒移除前,只會處理異步消息,處理完全部的異步消息後,就會處於堵塞。
若是想恢復處理同步消息,須要調用 removeSyncBarrier() 移除同步屏障:
在API 28的版本中,postSyncBarrier()已被標註hide,但依舊可在系統源碼中找到相關應用,好比:
爲了更快地響應UI刷新事件,在ViewRootImpl的scheduleTraversals函數中就用到了同步屏障:
參考文獻: