換個姿式,帶着問題看Handler

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中的同步屏障機制

0x一、Handler問題三連


1.Handler是什麼


答:Android定義的一套 子線程與主線程間通信消息傳遞機制面試


2.Handler有什麼用

答:把子線程中的 UI更新信息傳遞 給主線程(UI線程),以此完成UI更新操做。算法


3.爲何要用Handler,不用行不行


答:不行,由於android在設計之初就封裝了一套消息建立、傳遞、處理。若是不遵循就不能更新UI信息,就會報出異常(異步消息處理異常)數組

在Android中,爲了提升系統運行效率,沒有采用「線程鎖」,帶來了:安全

多個線程併發更新UI時的線程安全問題網絡

爲了安全保證UI操做是線程安全的,規定數據結構

只能在主線程(UI線程)中完成UI更新多線程

但,真的只能在UI線程中更新UI嗎?

上面這段代碼 直接在子線程中更新了UI,卻沒有報錯:

這是要打臉嗎?但若是在子線程中加句線程休眠模擬耗時操做的話:

程序就崩潰了,報錯以下:

翻譯一下異常信息:只有建立這個view的線程才能操做這個view。限於篇幅,這裏就不去跟源碼了,直接說緣由:

ViewRootImponCreate() 時還沒建立;
onResume()時,即ActivityThreadhandleResumeActivity() 執 行後才建立,
調用 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 進程 installProviderpublishContentProviders 調用到 AMS.publishContentProviders()
  • 超過這個時間,系統就會殺掉 Provider 進程。

0x二、Handler怎麼用


1.sendMessage() + handleMessage()


代碼示例以下

黃色部分會有以下警告

Handler不是靜態類可能引發「內存泄露」,緣由以及正確寫法等下再講。
另外,建議調用 Message.obtain() 函數來獲取一個Message實例,爲啥?點進源碼:

從源碼能夠看到obtain()的邏輯:

  • 一、判斷Message池是否爲空;
  • 二、不爲空,取出一個Message對象,池容量-1,返回;
  • 三、不然,新建一個Message對象,返回;

這樣能夠「避免重複建立多個實例對象」節約內存,還有,Message池實際上是一個「單鏈表結構」,定位到下述代碼能夠看到:池的容量爲50

而後問題來了,Message信息何時加到池中?

當Message 被Looper分發完後,會調用 recycleUnchecked()函數,回收沒有在使用的message對象。

若是你懂點數據結構的話,能夠看出這是「單鏈表的頭插法


2.post(runnable)


代碼示例以下

跟下post():

實際上調用了 sendMessageDelayed() 發送消息,只不過延遲秒數爲0,
那Runnable是怎麼變成Message的呢?跟下getPostMessage()

噢,獲取一個新的Message示例後,把 Runnable 變量的值賦值給 callback屬性


3.附:其餘兩個種在子線程中更新UI的方法


activity.runOnUiThread()

view.post() 與 view.postDelay()


0x三、Handler底層原理解析


終於來到稍微有點技術含量的環節,在觀摩源碼瞭解原理前,先說下幾個涉及到的類。


1.涉及到的幾個類



2.前戲


在咱們使用Handler前,Android系統已爲咱們作了一系列的工做,其中就包括了

建立「Looper」和「MessageQueue」對象

上圖中有寫:ActivityThreadmain函數是APP進程的入口,定位到 ActivityThread → main函數

定位到:Looper → prepareMainLooper函數

定位到:Looper → prepare函數

定位到:Looper → Looper構造函數

另外這裏的 quitAllowed 變量,直譯「退出容許」,具體做用是?跟下 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發送一個消息發生了什麼?


扯得有點遠了,拉回來,剛講到 ActivityThreadmain函數中調用 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的消息是怎麼處理的?


經過MessageQueuequeue.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的應用:
useof.org/java-open-s…


0x四、一些其餘問題


1.Looper在主線程中死循環,爲啥不會ANR?

答:上面說了,Looper經過queue.next()獲取消息隊列消息,當隊列爲空,會堵塞,
此時主線程也堵塞在這裏,好處是:main函數沒法退出,APP不會一啓動就結束!

你可能會問:主線程都堵住了,怎麼響應用戶操做和回調Activity聲明週期相關的方法?

答:application啓動時,可不止一個main線程,還有其餘兩個Binder線程ApplicationThreadActivityManagerProxy,用來和系統進程進行通訊操做,接收系統進程發送的通知。

  • 當系統受到因用戶操做產生的通知時,會經過 Binder 方式跨進程通知 ApplicationThread;
  • 它經過Handler機制,往 ActivityThreadMessageQueue 中插入消息,喚醒了主線程;
  • 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後,MessageQueueenqueueMessage()
按照 時間戳升序 將消息插入到隊列中,而Looper則按照順序,每次取出一枚Message進行分發,
一個處理完到下一個。這時候,問題來了:有一個緊急的Message須要優先處理怎麼破
你可能或說直接sendMessage()不就能夠了,不用等待立馬執行,看上去說得過去,不過可能
有這樣一個狀況:

一個Message分發給Handler後,執行了耗時操做,後面一堆本該到點執行的Message在那裏等着,這個時候你sendMessage(),仍是得排在這堆Message後,等他們執行完,再到你!

對吧?Handler中加入了「同步屏障」這種機制,來實現「異步消息優先執行」的功能。

添加一個異步消息的方法很簡單:

  • 一、Handler構造方法中傳入async參數,設置爲true,使用此Handler添加的Message都是異步的;
  • 二、建立Message對象時,直接調用setAsynchronous(true)

通常狀況下:同步消息和異步消息沒太大差異,但僅限於開啓同步屏障以前。
能夠經過 MessageQueuepostSyncBarrier 函數來開啓同步屏障:

行吧,這一步簡單的說就是:往消息隊列合適的位置插入了同步屏障類型的Message (target屬性爲null)
接着,在 MessageQueue 執行到 next() 函數時:

遇到target爲null的Message,說明是同步屏障,循環遍歷找出一條異步消息,而後處理。
在同步屏障沒移除前,只會處理異步消息,處理完全部的異步消息後,就會處於堵塞
若是想恢復處理同步消息,須要調用 removeSyncBarrier() 移除同步屏障:

在API 28的版本中,postSyncBarrier()已被標註hide,但依舊可在系統源碼中找到相關應用,好比:
爲了更快地響應UI刷新事件,在ViewRootImplscheduleTraversals函數中就用到了同步屏障:


參考文獻


相關文章
相關標籤/搜索