Android組件系列:Handler機(jī)制詳解

Overview

每個(gè)Android開發(fā)者都或多或少的了解過Android Handler機(jī)制,為了不浪費(fèi)大家時(shí)間,在文章開始之前,我認(rèn)為有必要說明一下本文的目標(biāo)受眾

適合人群:

1、Android新手開發(fā),對Handler機(jī)制還不是很熟悉,想要了解Handler內(nèi)部是如何運(yùn)行的

2、其他客戶端開發(fā),比如iOS、Windows開發(fā)等,想要了解在Android系統(tǒng)中是如何在非UI線程更新界面的

不適合人群:

1、中高級工程師,對Handler機(jī)制了解的很深入了,這篇文章純屬浪費(fèi)時(shí)間

2、覺得源碼過于枯燥,想要找一篇文章對照著看,本文可能不是很合適,因?yàn)槲恼轮胁⒉粫?huì)涉及太多的源碼

image.png

一、前言

Android Handler機(jī)制是每個(gè)Android開發(fā)者成長道路上一道繞不過去的坎,了解Handler機(jī)制對于解決開發(fā)中的遇到的卡頓檢測、ANR監(jiān)控,以及了解APP組件是如何運(yùn)行的等問題有著非常大的幫助

市面上已經(jīng)有許多講解Handler機(jī)制的文章,各種角度的都有,其中不乏有不少深度好文;本文不講解源碼(主要是講不過其他文章),從Android Handler機(jī)制的設(shè)計(jì)思想開始講起,由淺入深,帶你一步步走進(jìn)Handler內(nèi)部的實(shí)現(xiàn)原理

以下,enjoy:

二、Handler設(shè)計(jì)背景

在Android開發(fā)者官網(wǎng)對View的介紹有這樣一句話:

image.png

圖片來源:Android Developer

整個(gè)視圖樹是單線程的, 在任何視圖上調(diào)用任何方法時(shí),必須始終在 UI 線程上。如果在其他線程上工作并希望從該線程更新視圖的狀態(tài),則應(yīng)該使用Handler

不只是Android,在iOS開發(fā)者官網(wǎng)對UIKit介紹中同樣有類似提示:

image.png

圖片來源:Apple iOS Developer

除非另有說明,否則只能從應(yīng)用程序的主線程或主調(diào)度隊(duì)列中使用 UIKit 類。 此限制特別適用于從 UIResponder 派生的類或涉及以任何方式操作應(yīng)用程序用戶界面的類。

奇了怪了,Android和iOS作為一個(gè)有圖形用戶界面(Graphical User InterfaceI)的操作系統(tǒng),為什么都要求在主線程(UI線程)操作頁面?

1、GUI為什么要設(shè)計(jì)成單線程?

關(guān)于GUI為什么要設(shè)計(jì)成單線程這個(gè)問題,前Sun的副總裁Graham Hamilton在設(shè)計(jì)Java Swing時(shí),曾經(jīng)寫過一篇文章專門聊過:Multithreaded toolkits: A failed dream?

最近有人提出一個(gè)問題,“我們是否應(yīng)該讓 Swing 庫真正實(shí)現(xiàn)多線程?” 我個(gè)人覺得不應(yīng)該,下面闡述理由。

無法實(shí)現(xiàn)的夢想(Failed Dream)

借用 Vernor Vinge 的術(shù)語,在計(jì)算機(jī)科學(xué)中,某些想法是“無法實(shí)現(xiàn)的夢想”。這種想法初步看來很好,人們隔一段時(shí)間就會(huì)重新冒出這種想法,并為此花費(fèi)很多時(shí)間。通常在研究階段,事情進(jìn)展順利,有一些讓人感興趣的成果,差不多可以應(yīng)用到生產(chǎn)規(guī)模上了。只是總有些問題解決不了,解決了這邊的問題,那邊又有問題冒出來。

在我看來,多線程的 GUI 工具包,就是這種無法實(shí)現(xiàn)的夢想。在多線程環(huán)境中,任意一個(gè)線程都可以去更新按鈕(Button)、文本字段(Text Field)等 GUI 狀態(tài),這似乎是理所當(dāng)然、直截了當(dāng)?shù)淖龇?。任意線程去更新 GUI 狀態(tài),無非是加一些鎖,又有什么難的呢?實(shí)現(xiàn)過程中可能會(huì)有一些錯(cuò)誤,但我們可以修復(fù)這些錯(cuò)誤,對吧?可惜事實(shí)證明沒有這樣簡單。

多線程的 GUI 有種不可思議的趨勢,會(huì)不斷發(fā)生死鎖或者競爭條件。我第一次知道這趨勢,是在 80 年代初期,從 Xerox PARC 的 Cedar GUI 庫中工作的那些人中聽來的。這批人都十分聰明,也真正了解多線程編程。他們的 GUI 代碼時(shí)不時(shí)就有死鎖問題,這件事本身就很有趣。單獨(dú)這事不能說明什么,或者只是特殊情況。

只是這些年來不斷重復(fù)這一模式。人們最開始采用多線程,慢慢地,他們轉(zhuǎn)換到了事件隊(duì)列模型。“最好讓事件線程做 GUI 的工作。"

...略

為什么這么難

我們使用抽象來編寫代碼,很自然地會(huì)在每個(gè)抽象層中單獨(dú)上鎖。于是很不幸地,我們就遇到經(jīng)典的鎖定順序噩夢:有兩種不同類型的活動(dòng),按照相反的順序,試圖獲取鎖。因而死鎖不可避免。

...略

來自:Multithreaded toolkits: A failed dream?,原網(wǎng)頁已經(jīng)404,查看原文可以點(diǎn)擊下面兩個(gè)鏈接

簡單來說,多線程操作一個(gè)UI,很容易導(dǎo)致,或者極其容易導(dǎo)致反向加鎖和死鎖問題

我們通過用戶級的代碼去改變界面,如TextView.setText走的是個(gè)自頂向下的流程:

image.png

圖片來源:GUI為什么不設(shè)計(jì)為多線程

而系統(tǒng)底層發(fā)起的如鍵盤事件點(diǎn)擊事件走的是個(gè)自底向上的流程:

image.png

圖片來源:GUI為什么不設(shè)計(jì)為多線程

這樣就麻煩了,因?yàn)闉榱吮苊馑梨i,每個(gè)流程都要走一樣的加鎖順序,而GUI中的這兩個(gè)流程卻是完全相反的,如果每一層都有一個(gè)鎖的話加鎖就是個(gè)難以完成的任務(wù)了,而如果每一層都共用一個(gè)鎖的話,那就跟單線程沒區(qū)別了。

綜上,目前主流的帶有用戶界面(GUI)的操作系統(tǒng),除了DOS外,幾乎都是使用UI單線程模型的方案,即消息隊(duì)列機(jī)制

2、什么是消息隊(duì)列機(jī)制?

提到消息隊(duì)列,自然而然就會(huì)想到生產(chǎn)者-消費(fèi)者模式,在《從Android源碼角度談設(shè)計(jì)模式(三):行為型模式》一文中我們已經(jīng)介紹過了生產(chǎn)者-消費(fèi)者模式,還沒有看過的同學(xué)可以先看完再回來,這里來簡單回顧一下:

在一個(gè)生產(chǎn)者-消費(fèi)者模式中,通常會(huì)有三個(gè)角色

  • 消息隊(duì)列(single)

    負(fù)責(zé)保存消息,提供存取消息的功能

  • 生產(chǎn)者(multiple)

    負(fù)責(zé)生產(chǎn)消息,塞到共享的消息隊(duì)列中

  • 消費(fèi)者(multiple)

    負(fù)責(zé)從共享消息隊(duì)列中取出消息,執(zhí)行消息對應(yīng)的任務(wù)

image.png

這三個(gè)角色中,因?yàn)樯a(chǎn)者和消費(fèi)者要從同一個(gè)消息隊(duì)列取放消息,所以消息隊(duì)列的數(shù)量要求是唯一的,生產(chǎn)者和消費(fèi)者的數(shù)量可以任意

我們回到Android系統(tǒng),GUI框架多數(shù)使用單線程設(shè)計(jì),那么就要求所有的UI操作都只能發(fā)生在一個(gè)線程當(dāng)中,即:

消費(fèi)者線程只有一個(gè),且消費(fèi)者線程就是UI線程

到了這里,關(guān)于消息隊(duì)列機(jī)制應(yīng)該有個(gè)清晰的概念了,我們來總結(jié)一下:

Android、Swing、iOS等的GUI庫都使用消息隊(duì)列機(jī)制來處理繪制界面、事件響應(yīng)等消息

在這種設(shè)計(jì)中,每個(gè)待處理的任務(wù)都被封裝成一個(gè)消息添加到消息隊(duì)列里

消息隊(duì)列是線程安全的(消息隊(duì)列自己通過加鎖等機(jī)制保證消息不會(huì)在多線程競爭中丟失),任何線程都可以添加消息到這個(gè)隊(duì)列中

但是,只有主線程(UI線程)從中取出消息,并執(zhí)行消息的響應(yīng)函數(shù),這就保證了只有主線程才去執(zhí)行這些操作

小節(jié)完

3、Android中的消息隊(duì)列:Handler

前兩小節(jié)我們介紹了Android系統(tǒng)中為什么只能在UI線程更新界面的原因,最終發(fā)現(xiàn)不只是Android系統(tǒng),其他大部分有GUI框架的操作系統(tǒng)都是采用單線程消息隊(duì)列機(jī)制的設(shè)計(jì)。既然都是單線程設(shè)計(jì),那么想要在子線程更新UI就必須要通知主線程

在Android系統(tǒng)中,Handler就是這么一套提供在任意線程都可以發(fā)消息給UI線程的線程間通信工具

image.png

從圖中可以看到,Handler相當(dāng)于消息的生產(chǎn)者,每個(gè)Handler都持有共享MessageQueue的引用

當(dāng)調(diào)用Handler.sendMessage()方法發(fā)送消息時(shí),Handler就會(huì)把消息保存到共享消息隊(duì)列中

和普通消息隊(duì)列機(jī)制不同的時(shí),Android中的MessageQueue大小沒有閾值上限,所以理論上可以一直發(fā)消息到隊(duì)列,把內(nèi)存撐爆~

在Handler機(jī)制中,消息的消費(fèi)者是Looper,嚴(yán)謹(jǐn)點(diǎn)是調(diào)用Looper.loop()所在的線程,因?yàn)長ooper本質(zhì)上只是封裝對消息隊(duì)列的操作,Looper.loop()方法負(fù)責(zé)取出共享消息隊(duì)列里面的消息,然后交由Handler去執(zhí)行

這里又有一點(diǎn)和普通消息隊(duì)列機(jī)制不同,通常消息的消費(fèi)者會(huì)去執(zhí)行消息的響應(yīng)函數(shù);但在Android Handler機(jī)制中,消費(fèi)者本身并不執(zhí)行消息任務(wù),而是將消息取出后,再重新分發(fā)給消息的發(fā)送者執(zhí)行,也就是Handler,所以我們平時(shí)才說

在整個(gè)Handler的機(jī)制中,Handler首先是消息的生產(chǎn)者,其次才是消息的執(zhí)行者

除了發(fā)消息外,Android還為Handler增加了一些其他功能,比如子線程提交同步任務(wù)、異步消息和同步屏障等等

本小節(jié)的目的是了解Handler的設(shè)計(jì)背景,關(guān)于Handler其他功能這里就不展開介紹,在Handler系列的下一篇文章中會(huì)詳細(xì)剖析Handler內(nèi)部源碼

小結(jié)完

三、如何自己實(shí)現(xiàn)一套Handler機(jī)制?

在上一章節(jié)中,我們介紹了Handler的設(shè)計(jì)背景,本章節(jié)將會(huì)嘗試自己實(shí)現(xiàn)一套簡單的Handler機(jī)制

我們假設(shè)讀者已經(jīng)有一定的開發(fā)經(jīng)驗(yàn),并且使用過Handler,那么接下來就是枯燥的代碼時(shí)間

1、Handler成員介紹

在開擼之前,按照慣例,我們先來介紹一下組成Handler機(jī)制的幾位成員,以及它們常用的方法

1.1 Handler類

Handler類是應(yīng)用程序開發(fā)的入口,在消息隊(duì)列機(jī)制中,扮演著生產(chǎn)者的角色,同時(shí)還肩負(fù)著消息執(zhí)行者的重?fù)?dān),常用的方法有:

方法名稱 說明
sendMessage()系列 發(fā)送普通消息、延遲消息,最終調(diào)用queue.enqueueMessage()方法將消息存入消息隊(duì)列
post()系列 提交普通/延遲Runnable,隨后封裝成Message,調(diào)用sendMessage()存入消息隊(duì)列
dispatchMessage(Message msg) 分發(fā)消息,優(yōu)先執(zhí)行msg.callback(也就是runnable),其次mCallback.handleMessage(),最后handleMessage()

1.2 Looper類

Looper在消息隊(duì)列機(jī)制中扮演消費(fèi)者的角色,內(nèi)部持有共享的消息隊(duì)列,其本質(zhì)是封裝對消息隊(duì)列的操作,常用的方法只有兩個(gè):

方法名稱 說明
prepare() 創(chuàng)建消息隊(duì)列
loop() 遍歷消息隊(duì)列,不停地從消息隊(duì)列中取消息,消息隊(duì)列為空則等待

1.3 MessageQueue類

實(shí)際的共享消息隊(duì)列,提供保存和取出消息的功能,底層由鏈表實(shí)現(xiàn),常用方法就一個(gè):

方法名稱 說明
next() 獲取消息,三種情況 1. 有消息,且消息到期可以執(zhí)行,返回消息 2. 有消息,消息未到期,進(jìn)入限時(shí)等待狀態(tài) 3. 沒有消息,進(jìn)入無限期等待狀態(tài),直到被喚醒

1.4 Message類

消息的承載類,使用享元模式設(shè)計(jì),根據(jù)API不同緩沖池大小也不同,API 4時(shí)緩沖池大小為10,常用方法:

方法名稱 說明
obtain()系列 獲取一個(gè)消息實(shí)例
recycle() 回收消息實(shí)例

1.5 小結(jié)

至此,Handler的幾個(gè)主要成員類都介紹完了,有同學(xué)可能已經(jīng)發(fā)現(xiàn)了,成員介紹中沒有包含ThreadLocal類?

我個(gè)人認(rèn)為ThreadLocal是屬于Java并發(fā)模塊的內(nèi)容,Handler只是借用了ThreadLocal來保證MessageQueue在當(dāng)前線程線程的唯一性,就算不適用ThreadLocal對整個(gè)Handler機(jī)制也沒啥影響~

小節(jié)完

2、基于Object.wait()/notifiy()實(shí)現(xiàn)Handler機(jī)制

3.1小節(jié)介紹了Handler的幾個(gè)成員類,以及成員類中常用的方法,本章節(jié)將會(huì)用Java同步機(jī)制來實(shí)現(xiàn)Handler

話不多說,直接開擼

2.1 手寫消息隊(duì)列機(jī)制

前面的章節(jié)我們已經(jīng)了解過消息隊(duì)列機(jī)制的概念,這里直接來看代碼

首先我們來創(chuàng)建一個(gè)生產(chǎn)者,生產(chǎn)者線程中,我們寫了個(gè)死循環(huán)一直發(fā)送消息,來模擬用戶操作;每次發(fā)完消息后,喚醒可能在等待狀態(tài)的消費(fèi)者線程,然后將自己個(gè)兒置入限時(shí)等待狀態(tài)

image.png

然后再創(chuàng)建一個(gè)消費(fèi)者,在消費(fèi)者線程中,同樣是死循環(huán),不停的輪詢消息隊(duì)列,有消息就處理,沒消息就將自己置入無限期等待的狀態(tài),直到被喚醒

image.png

接著就是共享的消息隊(duì)列了,偷個(gè)懶,消息隊(duì)列就使用的是Java集合包中的Queue好了,來看最終的測試代碼:

image.png

打印結(jié)果:

image.png

在測試代碼中,兩個(gè)boss張三和李四時(shí)不時(shí)發(fā)指令給下屬,兩個(gè)下屬小明和小紅在不停的輪詢等待上司的指令,一個(gè)簡單的消息隊(duì)列機(jī)制就完成了;在不考慮延遲消息的情況下,加上測試代碼,整個(gè)消息隊(duì)列機(jī)制不到100行代碼就搞定了,還是比較簡單的

2.2 手寫Handler機(jī)制

2.1小節(jié)我們已經(jīng)用Object.wait()/notifiy()實(shí)現(xiàn)了消息隊(duì)列,實(shí)現(xiàn)Handler機(jī)制只需要在上面的消息隊(duì)列的基礎(chǔ)上稍微改動(dòng)一下就能完成:

同學(xué)們注意,我要開始變形了!??!

第一步,我們把消費(fèi)者線程中的邏輯挪到Looper當(dāng)中,把輪詢的任務(wù)放到loop()方法中;若取到了消息,模擬Android Handler機(jī)制,將消息分發(fā)給消息所屬的生產(chǎn)者者(Handler)去執(zhí)行

image.png

接著,將消費(fèi)者線程中獲取消息的邏輯抽離出來,放到MessageQueue的next()方法中

和上面的消費(fèi)者線程比較,這里加了一條邏輯:當(dāng)發(fā)送的消息時(shí)延遲消息時(shí),判斷消息是否到期,到期返回給Looper去分發(fā)(這里偷懶使用了Java集合包中的優(yōu)先級隊(duì)列PriorityQueue,來保證時(shí)間最小的消息排在最前面)

image.png

然后,把生產(chǎn)者線程中直接將消息存入消息隊(duì)列的操作的邏輯也抽出來,放到Handler的sendMessage()方法中

image.png

再然后,來看一眼Message類的設(shè)計(jì),加了個(gè)Handle類型的成員變量target

image.png

好!大功告成,一起看看測試代碼:

在main()方法中創(chuàng)建了handler1handler2,模擬用戶在主線程申請Handler的場景;隨后開啟倆子線程,讓子線程代碼使用剛剛創(chuàng)建的Handler發(fā)送消息,這樣,子線程使用該Handler發(fā)送的消息就會(huì)添加到主線程的消息隊(duì)列,等待主線程的Looper去處理

image.png

打印結(jié)果:

image.png

看,如果是普通任務(wù),loop()方法里面就直接分發(fā)掉了,延遲任務(wù)因?yàn)槭褂玫氖?code>PriorityQueue的緣故,會(huì)排到最后才放出來

到這里一個(gè)簡單的Handler機(jī)制就完成了,沒看懂的同學(xué)請?jiān)谠u論區(qū)扣1

關(guān)于此小節(jié)設(shè)計(jì)到的代碼在這里

ps:把代碼轉(zhuǎn)圖片是因?yàn)樵创a太長了我知道你們也懶得看~

2.3 小結(jié)

前兩小節(jié)分別介紹了如何手寫消息隊(duì)列機(jī)制和手寫一個(gè)簡單的Handler機(jī)制,我將上面的代碼調(diào)用流程梳理了一下,總結(jié)出一張簡版的流程圖:

image.png

圖中的每個(gè)成員類都只寫了兩個(gè)關(guān)鍵方法,Message類只是消息載體,所以就沒放它,我覺得介紹Handler大致流程差不多這幾個(gè)方法夠了,覺得流程不夠詳細(xì)的同學(xué)可以下載源文件自行添加,流程圖源文件已上傳到GitHub

小節(jié)完

四、Handler機(jī)制詳解

此小節(jié)設(shè)計(jì)之初的想法是要詳細(xì)的剖析Handler機(jī)的內(nèi)部源碼,在寫完了二、三章節(jié)后回頭看才發(fā)現(xiàn)

除了同步屏障與異步消息、IdleHandler、Callback等沒有講以外,整個(gè)Handler機(jī)制好像已經(jīng)講的差不多了-.-

既然沒太多可講的,那本節(jié)索性換個(gè)目標(biāo),來聊一聊Android Handler除了實(shí)現(xiàn)消息隊(duì)列機(jī)制外,還給我們提供了什么功能,它們是如何實(shí)現(xiàn)的,以及在使用Handler過程中我們有哪些需要特別注意的地方

1、除了提交消息到隊(duì)列,Handler還提供了哪些功能?

1.1 IdleHandler

IdleHandler是在Handler機(jī)制誕生之初就實(shí)現(xiàn)的機(jī)制,其存在的意義在于,提交一個(gè)不重要的任務(wù)單獨(dú)存放在MessageQueu中的mIdleHandlers變量中,當(dāng)消息隊(duì)列空閑時(shí)會(huì)執(zhí)行此任務(wù)

    /**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

IdleHandler要求返回bool類型的值,返回false表示執(zhí)行完該任務(wù)后會(huì)把它從集合中刪除,返回true表示該任務(wù)可以重復(fù)執(zhí)行

IdleHandler的處理邏輯在MessageQueue.next()方法中,我們來看一下Android 1.6版本中的處理邏輯:

// There was no message so we are going to wait...  but first,
// if there are any idle handlers let them know.
boolean didIdle = false;
if (idlers != null) {
    for (Object idler : idlers) {
        boolean keep = false;
        try {
            didIdle = true;
            keep = ((IdleHandler)idler).queueIdle();
        } catch (Throwable t) {
        }
        if (!keep) {//處理結(jié)果為false將其從集合中刪除
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }
}

IdleHandler使用方法大家都已經(jīng)很熟悉了,關(guān)注比較多的問題是:在什么場景下使用?

這里舉幾個(gè)三方庫和官方使用IdleHandler的例子,看看能不能從他們身上得到些啟發(fā)

  • ActivityThread.GcIdler

    直接看代碼

    final class GcIdler implements MessageQueue.IdleHandler {
            @Override
            public final boolean queueIdle() {
                doGcIfNeeded();
                return false;
            }
     }
    void doGcIfNeeded() {
            mGcIdlerScheduled = false;
            final long now = SystemClock.uptimeMillis();
            //獲取上次GC的時(shí)間
            if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
                //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
                BinderInternal.forceGc("bg");
            }
     }
    
    

    doGcIfNeeded方法理解起來很簡單,就是獲取上次GC的時(shí)間,判斷是否需要GC操作

  • 微信性能監(jiān)控框架:Matrix

    Matrix有好幾處使用到IdleHandler的地方,比如:

    IdleHandlerLagTracer.java

    WarmUpScheduler.java

    AndroidHeapDumper.java

    LooperMonitor.java

    以LooperMoitor舉例來說,這是監(jiān)聽Looper消息分發(fā)的類,微信這里利用IdlerHandler做了一個(gè)檢查的工作

        @Override
        public boolean queueIdle() {
            if (SystemClock.uptimeMillis() - lastCheckPrinterTime >= CHECK_TIME) {
                resetPrinter();
                lastCheckPrinterTime = SystemClock.uptimeMillis();
            }
            return true;
        }
    
    

    我們看到在queueIdle方法中返回了true,說明這個(gè)IdleHandler會(huì)被重復(fù)調(diào)用,每次調(diào)用queueIdle()方法時(shí),會(huì)去調(diào)用resetPrinter方法來檢查Looper中的printer對象是不是微信自定義的LooperPrinter

  • 內(nèi)存泄漏檢測框架:LeakCanary

    LeakCanary在ReferenceCleaner中用到了IdleHandler,源碼看起來是在onViewDetachedFromWindow()函數(shù)中注冊了IdleaHandler,注冊這個(gè)IdleHandler的目的是為了清除Android ims的bug,感興趣的同學(xué)可以自己點(diǎn)進(jìn)去看

1.2 異步消息與同步屏障

Android在API 16(4.1.1)增加了對異步消息和同步屏障消息的支持

所謂的同步屏障機(jī)制就是插入一個(gè)同步屏障消息到Looper的隊(duì)列頭部,準(zhǔn)確的說是拆入一個(gè)當(dāng)前時(shí)間的消息到隊(duì)列,如果隊(duì)列中有消息到期了但是還沒執(zhí)行,那么該同步屏障的消息會(huì)排在它后面;當(dāng)Looper調(diào)用next()獲取消息時(shí)候,發(fā)現(xiàn)隊(duì)列頭部是一個(gè)同步屏障信息,就會(huì)跳過所有同步消息,尋找所有的異步消息執(zhí)行,所以異步消息機(jī)制實(shí)質(zhì)上是一個(gè)對消息隊(duì)列的優(yōu)先級重新排列的機(jī)制

ps:關(guān)于異步消息與同步屏障在Handler、MessageQueue、Looper中處理邏輯這里沒講,全部加進(jìn)來太長了,感興趣的同學(xué)可以去網(wǎng)上找其他文章

1.3 Handler Callback機(jī)制

簡單來說,就是Handler在分發(fā)消息時(shí),提供了消息分發(fā)優(yōu)先級的選項(xiàng)給使用者,我們來看一下消息的分發(fā)邏輯:

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

優(yōu)先執(zhí)行msg.callback(也就是runnable),其次mCallback.handleMessage(),若callback返回false,最后執(zhí)行handleMessage()方法

2、使用Handler的注意事項(xiàng)

2.1 內(nèi)存泄漏

Handler引發(fā)的內(nèi)存泄漏是老生常談的話題,追根溯源的話其實(shí)和Handler本身沒關(guān)系,其本質(zhì)是生命周期長的組件引用生命周期短的組件,導(dǎo)致聲明周期短的組件明明已經(jīng)結(jié)束了,實(shí)例對象卻不能被回收

Activity中直接創(chuàng)建Handler,因?yàn)槭莾?nèi)部類的關(guān)系,該Handler會(huì)持有Activity的引用,若使用該Handler發(fā)送延時(shí)消息后銷毀Activity會(huì)發(fā)現(xiàn),在延時(shí)消息未執(zhí)行前,Activity包括其引用的對象都不會(huì)被釋放的

解決方案也比較簡單,聲明一個(gè)靜態(tài)內(nèi)部類即可,代碼如下:

public static class InternalHandler extends Handler {
    
    private WeakReference<Context> contextReference;

    public InternalHandler(WeakReference<Context> contextReference) {
        this.contextReference = contextReference;
    }
}

2.2 享元模式的坑

Handler中的Message類使用享元模式設(shè)計(jì),在《從Android源碼角度談設(shè)計(jì)模式(二):結(jié)構(gòu)型模式》一文中已經(jīng)解釋了享元模式本身會(huì)帶來數(shù)據(jù)不一致的問題

簡單來說,當(dāng)你將一個(gè)享元對象傳遞給子線程,因?yàn)镴ava是值傳遞,所以當(dāng)子線程使用到傳遞進(jìn)來的享元對象時(shí),這個(gè)對象可能正在回收池中,也可能已經(jīng)被取出供其他方法使用

體現(xiàn)在Handler機(jī)制中則是,Looper.loop()執(zhí)行完消息的分發(fā)后,會(huì)調(diào)用msg.recycle()將該消息實(shí)例對象回收,這時(shí)候就會(huì)有個(gè)問題,來看代碼:

 public static class InternalHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 250){
                //新建異步線程處理邏輯
                new Thread(() -> {
                    Thread.sleep(100*1000);//模擬耗時(shí)
                    System.out.println(msg.obj);//模擬消息使用
                }).start();
            }
        }
    }

如上,我們在拿到msg后,新起了一個(gè)線程去處理消息

來看Looper這邊的邏輯,loop()方法里面調(diào)用完msg.target.dispatchMessage()方法后緊接著就會(huì)回收消息對象實(shí)例

那么我們在子線程中拿到的對象引用,里面內(nèi)容的實(shí)際是空的,或者是該對象引用又已經(jīng)被其他線程在使用了,總之,在子線程中的消息不是原來的消息

3、Handler有哪些妙用

3.1 永不崩潰的APP

1. 使用方法

利用Handler機(jī)制攔截異常前兩年在網(wǎng)上還小火了一把,筆者剛知道Handler還可以這樣用的時(shí)候很開心,不會(huì)因?yàn)槲床东@異常導(dǎo)致APP崩潰了,我的績效有救了~

當(dāng)然,說永不崩潰言過其實(shí)了,它只能攔截Java層異常,native異常是沒辦法捕獲的,接下來我們來了解一下如何實(shí)現(xiàn)的

首先,先來看一下使用的方法,很簡單,我們在Application隨便找個(gè)方法加入以下代碼:

new Handler(Looper.getMainLooper()).post(() -> {
    while (true) {
        try {
            Looper.loop();
        } catch (Exception e) {
            //保存日志并上報(bào)..
        }
//      }catch (Throwable throwable){ }//想要連Error(如OOM)都一起攔截就用Throwable
    }
});

如上,只需短短幾行代碼,就可以捕獲Java層所有異常,怎么做到的?

2. 實(shí)現(xiàn)原理

在介紹實(shí)現(xiàn)原理之前,我們先來復(fù)習(xí)一下Java異常處理機(jī)制,當(dāng)一個(gè)異常發(fā)生時(shí):

  1. 虛擬機(jī)會(huì)在當(dāng)前出現(xiàn)異常的方法中,查找異常表,是否有合適的處理者來處理
  2. 如果當(dāng)前方法異常表不為空,并且異常符合處理者的 from 和 to 節(jié)點(diǎn),并且 type 也匹配,則虛擬機(jī)調(diào)用位于 target的調(diào)用者來處理
  3. 如果上一條未找到合理的處理者,則繼續(xù)查找異常表中的剩余條目
  4. 如果當(dāng)前方法的異常表無法處理,則向上查找(彈棧處理)剛剛調(diào)用該方法的調(diào)用處,并重復(fù)上面的操作
  5. 如果所有的棧幀被彈出,仍然沒有處理,則拋給當(dāng)前的 Thread,Thread 則會(huì)終止
  6. 如果當(dāng)前 Thread 為最后一個(gè)非守護(hù)線程,且未處理異常,則會(huì)導(dǎo)致虛擬機(jī)終止運(yùn)行
Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

我們來做個(gè)實(shí)驗(yàn),在Activity主動(dòng)拋出一個(gè)異常,看一下方法調(diào)用鏈,重點(diǎn)關(guān)注有沒有可以手動(dòng)try catch的地方

 java.lang.RuntimeException: 我崩潰了
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3639)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3796)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)//point 2
        at android.os.Looper.loop(Looper.java:288)//point 1
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
     Caused by: java.lang.RuntimeException: 我崩潰了

從下往上找,最下面幾個(gè),什么ZygoteInit、RuntimeInit這些系統(tǒng)的類聽起來就沒法操作

直到看到point 1位置的Looper.loop()方法,回顧一下第三章2.3小節(jié)中流程圖,loop()方法里面執(zhí)行的是死循環(huán),一直在輪詢消息隊(duì)列

那么我們再丟一個(gè)同樣執(zhí)行死循環(huán),并且調(diào)用Looper.loop()方法輪詢消息隊(duì)列的消息進(jìn)去,只要保證提交的這個(gè)消息不出錯(cuò),就永遠(yuǎn)不會(huì)出現(xiàn)上面的異常堆棧信息

并且,由于我在消息中調(diào)用了Looper.loop()方法,相當(dāng)于dispatch消息的代碼執(zhí)行在我提交的這個(gè)消息中,也就是說只要try catch住調(diào)用loop()的地方,在應(yīng)用內(nèi)任務(wù)Java異常我都可以捕獲了

好了,大概的實(shí)現(xiàn)原理已經(jīng)解釋清楚了,接下來看一下在msg中重復(fù)調(diào)用Looper.loop()方法后的方法棧調(diào)用鏈

//原先的方法調(diào)用鏈
ZygoteInit.main()
  -> RuntimeInit.MethodAndArgsCaller.run()
    -> ActivityThread.main()
        -> Looper.loop()
            -> MessageQueue.next()
//再次調(diào)用Looper.loop()方法后,調(diào)用鏈變成:
ZygoteInit.main()
-> RuntimeInit.MethodAndArgsCaller.run()
    -> Activity.main()
        -> LooperThread.loop() { //因?yàn)槭峭粋€(gè)線程內(nèi)調(diào)用,相當(dāng)于在Looper.loop方法上包了一層
        -> Looper.loop()
                -> MessageQueue.next()
        }

3. 小結(jié)

本小節(jié)介紹了讓Java代碼永不崩潰的實(shí)現(xiàn)原理,這套方案看起來比較牛逼,但是

不建議在生產(chǎn)環(huán)境中使用?。?!

在我剛知道這套方案時(shí),立馬就在項(xiàng)目中進(jìn)行了測試,測試結(jié)果和介紹的效果是一樣的,當(dāng)時(shí)真的覺得很牛逼,于是便興高采烈的發(fā)到生產(chǎn)環(huán)境

發(fā)版沒幾天就陸續(xù)收到反饋,頁面白屏沒數(shù)據(jù)、點(diǎn)擊沒反應(yīng)等等問題

后來查看上報(bào)日志,發(fā)現(xiàn)出問題的頁面的確存在bug,但是因?yàn)楸粩r截了導(dǎo)致了功能不能正常執(zhí)行,才會(huì)導(dǎo)致用戶操作沒反應(yīng)等

簡單來說,若某個(gè)生產(chǎn)數(shù)據(jù)的功能無法正常使用后,接下來依賴該數(shù)據(jù)的頁面都會(huì)產(chǎn)生一系列的問題

這反而會(huì)讓應(yīng)用中產(chǎn)生更多的不可控因素

最后經(jīng)過內(nèi)部討論后還是把這套方案放棄了,不能用這種蠻橫的方式對待bug

一刀切的方案過于野蠻,GitHub有個(gè)開源庫提供了更完善的解決方案,github.com/android-not…

小結(jié)完

3.2 ANR監(jiān)控

詳情可參考微信客戶端技術(shù)團(tuán)隊(duì)的文章:

五、Handler發(fā)展歷程

在文章的最后,我們來介紹一下Handler的發(fā)展歷史

下面的表格記錄了HandlerAndroid 1.6 (API 3) 一直到Android 12 (API 31)的演變過程:

Android 1.6(API 4)

這也許是你能找到最老版本的Handler源碼,此時(shí)Handler的完成度已經(jīng)很高了,特點(diǎn)是:

1、隊(duì)列空閑時(shí)等待以及喚醒方案用的是Java的Object.wait()/notfity()

調(diào)用MessageQueue的next方法獲取消息,這時(shí)候檢查隊(duì)列有沒有消息

  • 沒有消息調(diào)用this.wait()無限期等待
  • 有消息但消息未到期調(diào)用this.wait()傳入到期時(shí)間

調(diào)用MessageQueue.enqueueMessage()添加消息,消息加入隊(duì)列后會(huì)調(diào)用this.notifity()喚醒next()方法,這里有個(gè)bug是沒有判斷添加的消息要什么時(shí)候執(zhí)行,延遲消息也會(huì)喚醒

2、MessageQueue支持IdleHandler

IdleHandler是MessageQueue的內(nèi)部類,調(diào)用addIdleHandler(handler)方法將一個(gè)IdleHandler添加到mIdleHandlers集合中,消息隊(duì)列空閑時(shí)才會(huì)執(zhí)行

3、不支持異步消息和同步屏障消息

所有消息一視同仁,這也是早期Android設(shè)備體驗(yàn)不好的原因之一

4、Handler支持Callback回調(diào)

提到這個(gè)方法是因?yàn)樗容^重要,先來看一下消息分發(fā)的先后順序:

優(yōu)先執(zhí)行msg.callback(也就是runnable),其次mCallback.handleMessage(),最后handleMessage()

其中,調(diào)用mCallback.handleMessage()方法時(shí)會(huì)要求返回bool類型的值,這個(gè)值為true就不會(huì)向下分發(fā)給Handler的handleMessage()方法,為false會(huì)繼續(xù)向下分發(fā)

知道了分發(fā)邏輯之后我們就可以hook掉ActivityThread中的H類,傳入callback攔截我們想要的信息,進(jìn)而做組件化或監(jiān)控方面的工作

Android 2.3(API 9)

2.3比較大的變化是增加了native層的Handler消息機(jī)制

1、MessageQueue支持處理native層消息

MessageQueue通過mPtr變量保存NativeMessageQueue對象,從而使得MessageQueue成為Java層和Native層的樞紐,既能處理上層消息,也能處理native層消息

關(guān)于native層的Handler想要了解更多的同學(xué)看這里:

2、MessageQueue.next()方法中,空閑時(shí)等待方案從Object.wait()改為nativePollOnce()實(shí)現(xiàn)

由于加入native層Handler,隊(duì)列空閑時(shí)不能只判斷Java層的MessageQueue,MessageQueue將原先的this.wait()方法改成了調(diào)用native的nativePollOnce()方法,若大家都空閑,方法會(huì)阻塞到native的epoll_wait()方法中,等待喚醒

3、MessageQueue.enqueueMessage()方法中,喚醒方案從Object.notify()改為nativeWake()實(shí)現(xiàn)

參考上一小節(jié),這里還有個(gè)小細(xì)節(jié),Android 2.3之前只要調(diào)用enqueueMessage()方法就會(huì)調(diào)用this.notify()喚醒線程,哪怕加入的這個(gè)消息是個(gè)延遲消息要求一萬年后才執(zhí)行,在2.3的版本中的enqueueMessage()方法中修復(fù)了這個(gè)問題

Android 4.0(API 14)

1、Message增加flags屬性,用于標(biāo)識該消息是否已經(jīng)消費(fèi)過了,防止同一消息無限次提交

調(diào)用isInUse()方法可以查詢當(dāng)前消息是否使用過,這個(gè)flags后續(xù)也還會(huì)加入更多的含義

Android 4.1.1(API 16)

4.1.1版本變化略微大一些,主要是增加了對異步消息同步屏障消息的支持

1、Message支持設(shè)置為異步消息,@hide修飾

調(diào)用setAsynchronous(true)方法可以將Message設(shè)置為異步消息,判斷是否為異步消息的標(biāo)識保存在Message的成員變量flags中

2、MessageQueue支持處理異步消息

主要是在enqueueMessage()方法和next()方法中增加異步消息的處理邏輯

3、MessageQueue支持添加/刪除同步屏障消息

對應(yīng)方法為:enqueueSyncBarrier()removeSyncBarrier()

在MessageQueue的next()方法中也增加了對同步屏障消息的處理邏輯

4、MessageQueue支持quit()方法

4.1.1版本的退出邏輯是將MessageQueue的成員變量mQuiting設(shè)置為true,在調(diào)用MessageQueue.next()方法時(shí)檢查mQuiting變量值,為true則返回null給Lopper,Looper.loop()方法中判斷時(shí)null值直接結(jié)束當(dāng)前循環(huán)

注意這里并不會(huì)清空MessageQueue中的消息,也就是說若消息持有外部的強(qiáng)引用,那么會(huì)造成內(nèi)存泄漏

5、Lopper刪除成員變量mRun

這貨本來就沒啥用~,早期打印msg執(zhí)行日志的時(shí)候會(huì)帶上它

Android 4.2 (API 17)

1、Handler支持設(shè)置為異步Handler,@hide修飾

如下,新增Handler(boolean async)構(gòu)造函數(shù),使用該Handler發(fā)送的消息均為異步消息

public Handler(boolean async) {
    mAsynchronous = async;
}

2、Handler支持子線程提交同步任務(wù),新增runWithScissors()方法,@hide修飾

runWithScissors()方法接受一個(gè)Runnable和超時(shí)時(shí)間,調(diào)用此方法提交一個(gè)任務(wù)后:

1、若消息發(fā)送線程和Handler創(chuàng)建線程是同一線程,那么執(zhí)行Runnable的run方法

2、若消息發(fā)送線程和Handler創(chuàng)建線程不在同一線程,可以理解為子線程向主線程提交了一個(gè)任務(wù),任務(wù)提交后,子線程會(huì)進(jìn)入休眠狀態(tài)等待喚醒,一直等到任務(wù)執(zhí)行結(jié)束

注意?。。≡摲椒ú坏籃hide修飾,在代碼注釋也向開發(fā)者告知這是個(gè)危險(xiǎn)方法,不建議使用,因?yàn)閞unWithScissors()方法有兩個(gè)嚴(yán)重缺陷:

1、無法取消已提交的任務(wù),即使消息的發(fā)送線程已經(jīng)死亡,主線程仍然會(huì)取出消息隊(duì)列的任務(wù)執(zhí)行,但這時(shí)候運(yùn)行的程序是不符合我們的預(yù)期的

2、可能會(huì)造成死鎖:子線程向主線程(創(chuàng)建Handler的線程)提交了延遲任務(wù)后,子線程是處于等待被喚醒的狀態(tài),此時(shí)若主線程退出了loop循環(huán)并清空了消息隊(duì)列,那子線程提交的任務(wù)就永遠(yuǎn)不會(huì)被喚醒執(zhí)行,該任務(wù)持有的鎖永遠(yuǎn)不會(huì)被釋放,造成死鎖

Android 4.3 (API 18)

1、MessageQueue支持安全退出,quit(safe)方法

新增以下兩個(gè)方法,safe參數(shù)為true時(shí)調(diào)用removeAllFutureMessagesLocked()

  • removeAllMessagesLocked() :清空所有消息
  • removeAllFutureMessagesLocked():清空延遲消息,到期消息交給Handler分發(fā)

Android 6.0 (API 23)

1、MessageQueue支持監(jiān)聽文件描述符,對應(yīng)方法:addOnFileDescriptorEventListener()

這部分代碼我沒懂,目前的知識儲備不足以讓我看懂~ /哭唧唧.jpg

2、MessageQueue發(fā)送同步屏障消息方法改名,從enqueueSyncBarrier()改為postSyncBarrier()

我很認(rèn)真的確認(rèn)過,主要邏輯沒動(dòng)過,真的只是改個(gè)名

Android 8.0 (API 26)

1、Handler增加getMain()方法,用于獲取運(yùn)行在UI線程的Handler實(shí)例,@hide修飾

getMain()檢查成員變量MAIN_THREAD_HANDLER是否已經(jīng)保存了Handler實(shí)例,若MAIN_THREAD_HANDLER為空,則使用Looper.getManLooper()創(chuàng)建一個(gè)新的Hnadler實(shí)例,賦值給MAIN_THREAD_HANDLER變量,最后返回結(jié)果

注意,該方法只是返回一個(gè)運(yùn)行在UI線程的Handler,并不是ActivityThread中的成員變量mH?。?!

Android 9.0 (API 28)

1、Handler增加executeOrSendMessage()方法,@hide修飾

這個(gè)方法比較簡單,提供的功能和字面意思相同

判斷消息發(fā)送線程和消息消費(fèi)線程是同一線程,是的話調(diào)用Handler.dispatchMessage()方法分發(fā)消息,否則塞進(jìn)消息隊(duì)列等待被分發(fā)

2、Handler允許APP創(chuàng)建異步Handler

增加了靜態(tài)方法createAsync(),調(diào)用該方法會(huì)返回一個(gè)Handler實(shí)例,這個(gè)Handler實(shí)例就是異步Handler

Android 10.0 (API 29)

1、Looper增加setObserver(observer)方法,監(jiān)聽消息分發(fā)過程,@hide修飾

一共有3個(gè)方法

  1. Object messageDispatchStarting():發(fā)送消息前調(diào)用
  2. messageDispatched(token, msg):當(dāng)消息被 Handler 處理時(shí)調(diào)用
  3. dispatchingThrewException(token, msg, exception):在處理消息時(shí)拋出異常時(shí)調(diào)用

至此,Handler歷代更新的內(nèi)容都已梳理完成,每個(gè)方法都進(jìn)行了標(biāo)注,標(biāo)題中也加入了鏈接,有閱讀源碼需求的同學(xué)可以點(diǎn)擊查看

ps:個(gè)人整理難免會(huì)有疏漏,歡迎在留言區(qū)補(bǔ)充

六、總結(jié)

本文將Handler機(jī)制拆成了三個(gè)部分

第一部分是介紹Handler誕生的背景,Android為什么要設(shè)計(jì)出Handler

第二部分主要講如何手寫一套Handler機(jī)制,使用的是Java同步方法Object.wait()/notifiy()

第三部分介紹的是Handler除了實(shí)現(xiàn)消息隊(duì)列外,還提供了哪些功能?以及開發(fā)中使用Handler有哪些需要注意的地方

希望每位同學(xué)在看完本篇文章后都能夠有所收獲 -.-

全文完

七、參考資料

本文轉(zhuǎn)自 https://juejin.cn/post/7084544971713282056,如有侵權(quán),請聯(lián)系刪除。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容