手?jǐn)]一個(gè)Android經(jīng)典線程通信框架:Handler

前言


2022年已過1/4,時(shí)間過的真是快。近些年大Android的發(fā)展也很是迅速,尤其是遵循MVVM或者MVI架構(gòu)下懈叹,使用Jetpack + Kotlin + Corroutine + Flow的組合,大大提升了Android應(yīng)用的開發(fā)效率萝毛。然而项阴,類似的效率的提升往往是通過層層封裝,隱藏底層原理笆包,簡化調(diào)用,從而達(dá)到降低開發(fā)的上手門檻目的的略荡。作為一個(gè)有品位的開發(fā)者庵佣,又怎能滿足于只了解上層的API調(diào)用。本文我們就來通過實(shí)例來試著聊一聊Android經(jīng)典線程通信框架Handler的基本原理汛兜。

目的


通過實(shí)現(xiàn)一個(gè)簡單的Handler框架巴粪,試圖解釋其底層的工作原理。

背景知識


  • 線程間通信
    即不同線程之間交換信息粥谬,Java中常見的有:

    1. Object
      wait(), notify()/notifyAll()
    2. LockSupport
      park(), uppark()
    3. Condition
      await(), signal()/signalAll()
  • Android的Handler工作原理
    簡言之肛根,重復(fù)執(zhí)行“發(fā)送消息-分發(fā)消息-處理消息”。
    更準(zhǔn)確的問題描述應(yīng)該是:Android的基于Handler + Message + MessageQueue + Looper的消息機(jī)制是怎么工作的漏策?

Handler借助handler#sendMessage(message)方法派哲,把消息Message對象插入消息隊(duì)列MessageQueue對象中,同時(shí)Looper對象循環(huán)往復(fù)地從MessageQueue對象中取下一個(gè)消息Message對象分發(fā)給Looper對象所在線程去處理掺喻,如此循環(huán)往復(fù)芭届。

注意: 同一個(gè)線程中,Looper和MessageQueue有且只有一個(gè)實(shí)例感耙,能且只能在該線程內(nèi)部實(shí)例化褂乍。Handler可有多個(gè)實(shí)例,可在任意線程內(nèi)實(shí)例化即硼,但前需保證實(shí)例化動(dòng)作所在線程的Looper已被初始化逃片。另外,也可以通過調(diào)用帶Looper參數(shù)的Handler構(gòu)造器實(shí)例化只酥,以達(dá)到關(guān)聯(lián)指定線程(Looper所在)的目的褥实。

從上述描述中,我們可以得出幾個(gè)關(guān)鍵的點(diǎn):
消息對象:保存消息的數(shù)據(jù)結(jié)構(gòu)(會保存發(fā)送者的Handler對象引用)
發(fā)送消息:調(diào)用者通過Handler#sendMessage(message)發(fā)送消息(發(fā)到哪里层皱?緩存MessageQueue
緩存消息MessageQueue保存由外部發(fā)送來的消息(怎么保存性锭?類似單向鏈表結(jié)構(gòu))
讀取消息Looper#loop()內(nèi)調(diào)用MessageQueue.next()讀取消息(沒消息了怎么辦?阻塞)
分發(fā)消息:取到消息后叫胖,執(zhí)行消息對象持有的Handler對象的Handler#dispatchMessage(message)方法草冈。(運(yùn)行在哪個(gè)線程?Looper對象所在的線程)
處理消息Handler#dispatchMessage(message)內(nèi)再調(diào)用Handler#handleMessage(message)方法,后者便正是調(diào)用者常重寫的方法怎棱。
循環(huán)往復(fù)Looper#loop()內(nèi)部是一個(gè)for(;;){...}哩俭,一直執(zhí)行,提供循環(huán)往復(fù)的驅(qū)動(dòng)力拳恋。
線程內(nèi)Looper對象唯一性:依靠ThreadLocal保證每個(gè)線程內(nèi)部Looper唯一凡资,即只被實(shí)例化一次。
線程內(nèi)MessageQueue對象唯一性:被Looper實(shí)例化并持有谬运,間接保證了唯一性隙赁。

  • Android主線程Handler使用

應(yīng)用啟動(dòng)時(shí),ActivityThread.main(String[])內(nèi)部會提前初始化Looper梆暖。故在主線程內(nèi)部伞访,可直接使用Handler的無參數(shù)構(gòu)造器實(shí)例化。

  • Android子線程Handler使用

如上所述轰驳,子線程使用Handler需要先確保Looper已被初始化厚掷,但子線程默認(rèn)是沒有初始化Looper的,故需在子線程執(zhí)行的一開始主動(dòng)調(diào)用Looper.prepare()Looper.loop()级解。此后則可如在主線程一般任意實(shí)例化Handler冒黑。

  • Android的HandlerThread原理

即封裝了HandlerThread子類。HandlerThread#start()被調(diào)用后勤哗,該線程內(nèi)部會初始化Looper實(shí)例抡爹。其他線程可從該HandlerThread對象中取出Looper實(shí)例,并用它構(gòu)造出新的Handler對象俺陋。此后其他線程可借助該Handler對象豁延,調(diào)用Handler#sendMessage(message)給HandlerThread的線程發(fā)送消息。

實(shí)現(xiàn)

思路

在了解了上述的Android的Handler工作原理后腊状,自己實(shí)現(xiàn)一個(gè)簡單的消息框架就有方向了诱咏,無非就是解決上述中提到的“消息對象”,“發(fā)送消息”缴挖,“緩存消息”……關(guān)鍵點(diǎn)即可袋狞。

線程內(nèi)Looper對象唯一性線程內(nèi)MessageQueue對象唯一性讀取消息是需要著重考慮的映屋。

編碼

Message

package com.custom.handler

data class Message(
    val what: Int = 0,
    val args: Any? = null,
    val block: (() -> Unit)? = null,
) {
    companion object {
        val HEAD_MESSAGE: Message = Message()
    }

    var target: Handler? = null
    var next: Message? = null
}

MessageQueue

package com.custom.handler

import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock

class MessageQueue {
    private val queueLock = ReentrantLock()
    private val queueCondition = queueLock.newCondition()

    private val messageLock = ReentrantReadWriteLock()
    private val messageReadLock = messageLock.readLock()
    private val messageWriteLock = messageLock.writeLock()

    private var currentMessage: Message = Message.HEAD_MESSAGE
        get() = messageReadLock.withLock { field }
        set(value) = messageWriteLock.withLock { field = value }

    fun next(): Message {
        while (true) {
            queueLock.withLock {
                if (currentMessage.next == null) {
                    println("[${Thread.currentThread().name}] Message queue is empty, then it is going to await...")
                    queueCondition.await()
                    println("[${Thread.currentThread().name}] Message queue wakes up.")
                }
            }

            val nextMessage = currentMessage.next!!
            nextMessage.next = null
            currentMessage = nextMessage
            return nextMessage
        }
    }

    fun enqueue(msg: Message) {
        messageWriteLock.withLock {
            msg.next = null
            currentMessage.next = msg
            queueLock.withLock { queueCondition.signalAll() }
        }
    }
}

Looper

package com.custom.handler

import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock

class Looper private constructor(thread: Thread) {
    internal val mMessageQueue = MessageQueue()
    private val mThread = thread
    private val quitFlagReentrantReadWriteLock = ReentrantReadWriteLock()
    private val quitFlagReadLock = quitFlagReentrantReadWriteLock.readLock()
    private val quitFlagWriteLock = quitFlagReentrantReadWriteLock.writeLock()
    private var quitFlag: Boolean? = null
        get() = quitFlagReadLock.withLock { field }
        set(value) = quitFlagWriteLock.withLock { field = value }

    companion object {
        private val looperMapReentrantReadWriteLock = ReentrantReadWriteLock()
        private val looperMapReadLock = looperMapReentrantReadWriteLock.readLock()
        private val looperMapWriteLock = looperMapReentrantReadWriteLock.writeLock()
        private val looperMap: MutableMap<Thread, Looper> = mutableMapOf()

        val looper: Looper?
            get() = looperMapReadLock.withLock { looperMap[Thread.currentThread()] }

        fun prepare(): Looper {
            require(looper == null) { "Looper.prepare() must be called once in each thread." }
            val thread = Thread.currentThread()
            val threadLooper = Looper(thread)
            looperMapWriteLock.withLock { looperMap[thread] = threadLooper }
            return threadLooper
        }

        fun loop() {
            requireNotNull(looper) { "Looper.prepare() must be called before Looper.loop()" }
            looper?.quitFlag = false
            while (looper?.quitFlag == false) {
                val me = looper!!.mMessageQueue
                val msg = me.next()
                kotlin.runCatching {
                    println("[${Thread.currentThread().name}] Dispatching message is started.")
                    msg.target?.dispatchMessage(msg)
                    println("[${Thread.currentThread().name}] Dispatching message is stopped. ")
                }.onFailure {
                    it.printStackTrace()
                }
            }
        }
    }

    fun quit() {
        quitFlag = true
        looperMap.remove(Thread.currentThread())
    }
}

Handler

package com.custom.handler

open class Handler {
    private var interceptor: Interceptor? = null
    private lateinit var mLooper: Looper

    constructor() {
        val looper = requireNotNull(Looper.looper) { "No looper found in current thread." }
        Handler(looper)
    }

    constructor(looper: Looper) {
        mLooper = looper
    }

    interface Interceptor {
        fun handleMessage(message: Message?): Boolean
    }

    open fun dispatchMessage(message: Message?) {
        println("[${Thread.currentThread().name}] onDispatchMessage: message=$message")
        message ?: return
        if (message.block == null) {
            if (interceptor?.handleMessage(message) != true){
                handleMessage(message)
            }
            return
        }
        message.block.invoke()
    }

    open fun handleMessage(message: Message?) {
        println("[${Thread.currentThread().name}] onReceiveMessage: message=$message")
    }

    fun sendMessage(message: Message?) {
        println("[${Thread.currentThread().name}] onSendMessage: message=$message")
        message?.target = this
        mLooper.mMessageQueue.enqueue(message ?: return)
    }

    fun quit(){
        mLooper.quit()
    }
}

[擴(kuò)展] HandlerThread

import com.custom.handler.Looper
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class HandlerThread(name:String):Thread(name) {
    private val lock = ReentrantLock()
    private val condition = lock.newCondition()
    private var mLooper: Looper? = null
    val looper: Looper
        get() {
            if (mLooper == null) {
                lock.withLock { condition.await() }
            }
            return requireNotNull(mLooper)
        }

    override fun run() {
        super.run()
        mLooper = Looper.prepare()
        lock.withLock { condition.signalAll() }
        Looper.loop()
    }
}

[測試] Main
由于HandlerThread已經(jīng)處理了子線程初始化Looper的操作苟鸯,因此,使用該類直接測試即可棚点。

import com.custom.handler.Handler
import com.custom.handler.Message

fun main(args: Array<String>) {
    testHandler()
}

private fun testHandler(){
    val handlerThread = HandlerThread("Thread-1").apply { start() }
    val handler = object : Handler(handlerThread.looper) {
        override fun handleMessage(message: Message?) {
            super.handleMessage(message)
        }
    }
    for (i in 0..3){
        if (i == 3) {
            handler.quit()
            return
        }
        Thread.sleep(3000)
        println("\n")
        handler.sendMessage(Message(100, "message from Main-Thread"))
    }
}
// console output
[Thread-1] Message queue is empty, then it is going to await...

[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped. 
[Thread-1] Message queue is empty, then it is going to await...

[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped. 
[Thread-1] Message queue is empty, then it is going to await...

[main] onSendMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Message queue wakes up.
[Thread-1] Dispatching message is started.
[Thread-1] onDispatchMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] onReceiveMessage: message=Message(what=100, args=message from Main-Thread, block=null)
[Thread-1] Dispatching message is stopped. 

Process finished with exit code 0

Q&A


  1. Handler是怎么實(shí)現(xiàn)線程切換的早处?
    就個(gè)人理解而言,所謂“線程切換”瘫析,即就是實(shí)現(xiàn)線程間的通信即可砌梆。此處默责,我們姑且把“線程”理解為一個(gè)無限循環(huán)。外界的消息/操作要想插入到這個(gè)“線程”咸包,可想象為把消息/操作插入到無限循環(huán)中桃序,如此以來便完成了“線程切換”。
    消息插入無限循環(huán):即為存在競爭關(guān)系的變量的同步問題烂瘫。線程外部修改變量媒熊,線程內(nèi)部讀取變量》乇龋可通過背景知識中提到的各種線程間通信的方法來實(shí)現(xiàn)芦鳍。
    操作插入無限循環(huán):即為注冊回調(diào)問題。

發(fā)送消息時(shí)葛账,調(diào)用Handler#sendMessage(message)怜校,該方法運(yùn)行在發(fā)生調(diào)用動(dòng)作所在的線程(簡稱,調(diào)用線程)注竿;處理消息時(shí),會先后調(diào)用Handler#dispatchMessage(messge)魂贬、Handler#handleMessage(message)巩割,此兩方法運(yùn)行在Looper.loop()的無限循環(huán)中,即Looper的實(shí)例對象所屬的線程(簡稱付燥,處理線程)宣谈。而發(fā)送消息的本質(zhì)是:調(diào)用線程向處理線程的Looper內(nèi)的消息隊(duì)列MessageQueue中插入一個(gè)消息對象;處理消息的本質(zhì)是:處理線程取出消息再調(diào)用消息實(shí)例對象持有的Handler實(shí)例的Handler#handleMessage(message)键科,參數(shù)即為當(dāng)前取出的消息實(shí)例對象闻丑。

  1. 無消息時(shí),MessageQueue#next()如何被阻塞勋颖?
    方法內(nèi)部會先調(diào)用android.os.MessageQueue#nativePollOnce(ptr, timeoutMillis)方法嗦嗡。若當(dāng)前沒有消息,則nativePollOnce(ptr, timeoutMillis)會從native層使用epollepoll_wait機(jī)制阻塞當(dāng)前線程饭玲,至此MessageQueue.next()也就被阻塞侥祭。
Message next () {
    ...
    for (;;) {
        ...
        android.os.MessageQueue#nativePollOnce
        ...
        return nextMsg
    }
}
  1. 有新消息時(shí),MessageQueue#next()如何被喚醒茄厘?
    當(dāng)消息隊(duì)列被插入新消息時(shí)矮冬,MessageQueue#enqueueMessage(msg, when)會被調(diào)用,消息插入完成后次哈,其內(nèi)部android.os.MessageQueue#nativeWake(ptr)會被調(diào)用胎署,此時(shí)原本被android.os.MessageQueue#nativePollOnce(ptr, timeoutMillis)阻塞的線程就會喚醒,并開始執(zhí)行取消息和處理消息的流程窑滞。
boolean enqueueMessage (message, when) {
    ...
    //insert message
    ...
    nativeWake(mPtr)
}

寫在最后


文中提到的ThreadLocal琼牧,簡言之恢筝,就是JDK提供的一種存儲“線程內(nèi)部”變量的手段。Android的基于Handler的消息機(jī)制障陶,也正是借助這一點(diǎn)來保存各個(gè)線程內(nèi)部Looper的滋恬。當(dāng)然,若在不考慮性能優(yōu)化等各種因素條件下抱究,不使用ThreadLocal也是可以實(shí)現(xiàn)類似功能的恢氯,如本文所實(shí)現(xiàn)的消息框架則是使用了Map的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)的。 當(dāng)然鼓寺,本文的簡單實(shí)現(xiàn)也旨在解釋清楚Android消息機(jī)制的原理勋拟。

附:


源碼地址: https://github.com/heychinje/Simple-Handler
關(guān)于Android中的epoll:https://stackoverflow.com/questions/38818642/android-what-is-message-queue-native-poll-once-in-android

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市妈候,隨后出現(xiàn)的幾起案子敢靡,更是在濱河造成了極大的恐慌,老刑警劉巖苦银,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啸胧,死亡現(xiàn)場離奇詭異,居然都是意外死亡幔虏,警方通過查閱死者的電腦和手機(jī)纺念,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來想括,“玉大人陷谱,你說我怎么就攤上這事∩冢” “怎么了烟逊?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長铺根。 經(jīng)常有香客問我宪躯,道長,這世上最難降的妖魔是什么夷都? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任眷唉,我火速辦了婚禮,結(jié)果婚禮上囤官,老公的妹妹穿的比我還像新娘冬阳。我一直安慰自己,他們只是感情好党饮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布肝陪。 她就那樣靜靜地躺著,像睡著了一般刑顺。 火紅的嫁衣襯著肌膚如雪氯窍。 梳的紋絲不亂的頭發(fā)上饲常,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音狼讨,去河邊找鬼贝淤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛政供,可吹牛的內(nèi)容都是我干的播聪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼布隔,長吁一口氣:“原來是場噩夢啊……” “哼离陶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衅檀,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤招刨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哀军,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉眶,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年杉适,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沦寂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡淘衙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腻暮,到底是詐尸還是另有隱情彤守,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布哭靖,位于F島的核電站具垫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏试幽。R本人自食惡果不足惜筝蚕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铺坞。 院中可真熱鬧起宽,春花似錦、人聲如沸济榨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽擒滑。三九已至腐晾,卻和暖如春叉弦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背藻糖。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工淹冰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巨柒。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓樱拴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親潘拱。 傳聞我的和親對象是個(gè)殘疾皇子疹鳄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容