Android 消息機(jī)制


一遗遵、消息模型

生產(chǎn)者-消費(fèi)者模型,Android 架構(gòu)逸嘀,線程間通信车要,基于消息機(jī)制,消費(fèi)者從隊(duì)列獲取崭倘、處理消息翼岁,實(shí)現(xiàn)休眠與喚醒,生產(chǎn)者向隊(duì)列插入消息司光,通知消費(fèi)者琅坡。

生產(chǎn)者-消費(fèi)者模型

ActivityThread 類 main() 方法,主線程創(chuàng)建 Looper 和 Queue残家,Looper.loop() 方法榆俺,啟動循環(huán),主線程 main() 方法未結(jié)束坞淮,(運(yùn)行+休眠)茴晋,否則拋出異常退出。

Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");

隊(duì)列是空時回窘,消費(fèi)者線程休眠诺擅,防止 CPU 空跑占用資源,不空時毫玖,消費(fèi)者被喚醒掀虎,生產(chǎn)者+消費(fèi)者,兩個線程 + 阻塞隊(duì)列 BlockQueue 可實(shí)現(xiàn)付枫,利用 Java 中 Condition 的 await 和 signal 機(jī)制烹玉。

Android,采用 Native 層 epoll 方案阐滩。

Handler二打、MQ 和 Looper 類結(jié)構(gòu)

Handler 類,消息構(gòu)建掂榔、發(fā)送继效、回調(diào)症杏。
Message 類,傳遞消息對象瑞信。
MessageQueue 類厉颤,隊(duì)列,控制消息吞吐凡简。
Looper 類逼友,線程循環(huán) loop。

構(gòu)建線程的消息處理機(jī)制秤涩,在線程的 run() 方法啟動 Looper 類的 prepare() 方法帜乞,準(zhǔn)備一個線程本地的 Looper 實(shí)例。
ThreadLocal 類對 Looper 對象進(jìn)行線程隔離筐眷。

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

Looper 類構(gòu)造方法黎烈,創(chuàng)建消息隊(duì)列,MessageQueue 類構(gòu)造方法匀谣,初始化 Native 層隊(duì)列照棋。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

Handler 類構(gòu)造方法,支持 綁定特定線程 Looper (入?yún)?振定,默認(rèn) Handler 對象創(chuàng)建線程 Looper必怜,

public Handler(Callback callback, boolean async) {
    ...
    mLooper = Looper.myLooper();
    //mLooper是空會拋出異常
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

Handler 綁定的 Looper,決定它向哪一個 MQ 發(fā)送消息后频。

Native 層 NativeMessageQueue 類梳庆,Looper 類,控制線程休眠和喚醒卑惜。

二膏执、工作過程

消息機(jī)制工作過程

Handler 類,sendMessage() 方法露久,將消息插入 MessageQueue更米。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

將消息 target 設(shè)置成調(diào)用者 Handler 對象,每個消息都引用處理它的回調(diào)類毫痕。任何線程都可以將消息插入隊(duì)列征峦,MessageQueue 類的 enqueueMessage(),方法內(nèi)部 synchronized 代碼同步消请。

消費(fèi)者線程 Looper 類栏笆,loop()方法,循環(huán)遍歷臊泰。

public static void loop() {
    //當(dāng)前線程Looper
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    //循環(huán)
    for (;;) {
        Message msg = queue.next(); // 這里可能休眠
        if (msg == null) {
            // 沒有消息蛉加,代表消息隊(duì)列已經(jīng)退出,結(jié)束循環(huán)。
            return;
        }
        //Message消息處理
        try {
            msg.target.dispatchMessage(msg);
        } finally {
        }
        ...
    }
}

通過隊(duì)列 next() 方法针饥,獲得消息厂抽,消息 target (發(fā)送者 Handler )回調(diào),如果隊(duì)列是空丁眼,進(jìn)入休眠狀態(tài)筷凤。
Handler 類 dispatchMessage() 方法。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

處理優(yōu)先級户盯。
消息體 Callback > Handler 類內(nèi)部 Callback > 重寫的 handleMessage() 方法嵌施。

三饲化、底層原理

消息機(jī)制核心原理在 Native 層莽鸭。

消息機(jī)制底層原理圖

1,Looper吃靠,綁定隊(duì)列硫眨,消息隊(duì)列 MessageQueue,next() 方法巢块,查詢消息礁阁、進(jìn)入底層休眠。

Message next() {
    final long ptr = mPtr;
    //底層消息隊(duì)列已經(jīng)銷毀族奢,直接退出循環(huán)姥闭。
    //Looper.loop收到null時,也將退出for循環(huán)越走,結(jié)束線程棚品。
    if (ptr == 0) {
        return null;
    }
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;//第一次進(jìn)入時,不設(shè)置休眠等待時間廊敌。
    for (;;) {
        //JNI方法铜跑,底層休眠,第一次循環(huán)休眠時間是0骡澈。
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 如果隊(duì)列第一個是同步柵欄消息锅纺,則跳過后續(xù)的同步消息,直接找到異步消息執(zhí)行肋殴,確保異步消息的優(yōu)先級高囤锉。
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            //若不是同步柵欄,msg就是隊(duì)列的第一個消息护锤。
            if (msg != null) {
                if (now < msg.when) {
                    // 發(fā)現(xiàn)此時還未到消息的執(zhí)行時間官地,設(shè)置差值,將繼續(xù)循環(huán)蔽豺,休眠差值時間区丑。
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 此刻已經(jīng)到達(dá)消息設(shè)定執(zhí)行時間,消息返回給loop方法。
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 若消息隊(duì)列已經(jīng)空了沧侥,設(shè)置無限等待休眠可霎,直到手動喚醒,在插入時宴杀。
                nextPollTimeoutMillis = -1;
            }
            ...
        }
        ...
    }
}

nativePollOnce() 方法癣朗,Native 層消息隊(duì)列指針 mPtr,根據(jù)當(dāng)前時間和隊(duì)列內(nèi)消息 delay 執(zhí)行的目標(biāo)時間旺罢,計(jì)算休眠時間 nextPollTimeoutMillis旷余,通知底層。
喚醒時扁达,從隊(duì)列 get 消息正卧,返回 Looper 類 loop() 處理。

nativePollOnce() 通知 Native 層 Looper跪解,調(diào)用 Looper 類 pollInner() 方法炉旷。

int Looper::pollInner(int timeoutMillis) {
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    //休眠
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    //遍歷事件數(shù)量
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        //若是mWakeEventFd句柄發(fā)生的事件,如向其寫入了數(shù)據(jù)叉讥。
        if (fd == mWakeEventFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            } 
        } 
    }
Done: ;
    // 處理mMessageEnvelopes中的message窘行,拿到handler。
    // 這里面的消息是在底層發(fā)送的图仓,底層Looper#sendMessage方法罐盔。
    mNextMessageUptime = LLONG_MAX;
    while (mMessageEnvelopes.size() != 0) {
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
        if (messageEnvelope.uptime <= now) {
            { 
                sp<MessageHandler> handler = messageEnvelope.handler;
                Message message = messageEnvelope.message;
                mMessageEnvelopes.removeAt(0);
                mSendingMessage = true;
                mLock.unlock();
                //執(zhí)行MessageHandler的handleMessage消息。
                handler->handleMessage(message);
            }

            mLock.lock();
            mSendingMessage = false;
            result = POLL_CALLBACK;
        } else {
            mNextMessageUptime = messageEnvelope.uptime;
            break;
        }
    }
    mLock.unlock();
    return result;
}

利用 epoll_wait() 方法救崔,實(shí)現(xiàn)線程休眠惶看,參數(shù) mEpollFd 是 epoll 句柄,由 epoll_create() 方法創(chuàng)建帚豪,參數(shù) eventItems碳竟,監(jiān)聽事件的集合,timeoutMillis 休眠時間狸臣。
該 timeout 由上層計(jì)算莹桅,超時自動喚醒,回到上層消息處理烛亦。

描述符 mWakeEventFd诈泼,監(jiān)聽注冊流的事件,(非 timeout )煤禽,Java 層插入新消息時寫入該事件铐达。
Java 層隊(duì)列是空時,不設(shè)定 timeout檬果,timeoutMillis 值-1瓮孙,epoll_wait() 方法一直休眠唐断。

舉例,Java 層消息隊(duì)列第一個消息的執(zhí)行時間 when 超過當(dāng)前時間1分鐘杭抠,表示消息延遲1分鐘執(zhí)行脸甘,通過 nativePollOnce() 方法,通知 Native 層 Looper 在 epoll_wait() 方法位置休眠1分鐘偏灿。

Native 層丹诀,Looper 類的 pollInner() 方法提供給上層調(diào)用,(Native 層需要利用 Looper 機(jī)制的地方也會調(diào)用翁垂,在Done代碼塊铆遭,處理 Native 層 Looper 類 sendMessage() 方法發(fā)送的消息,MessageHandler 類負(fù)責(zé)回調(diào))沿猜。

2枚荣,Handler 類,調(diào)用隊(duì)列的 enqueueMessage() 方法邢疙,插入消息棍弄。

boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        //消息隊(duì)列是空,或立即執(zhí)行疟游,或按時間排序插入頭部。設(shè)置喚醒標(biāo)志痕支。
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            //若第一個消息是同步柵欄(即沒有派發(fā)目標(biāo))颁虐,且插入消息是異步消息。
            //不管怎樣卧须,都需要喚醒
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                //隊(duì)里已經(jīng)有異步消息啦另绩,不需要喚醒。
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

當(dāng)隊(duì)列是空或 when 是0花嘶,或執(zhí)行時間 when 小于頭部消息時間笋籽,插入到頭部位置,根據(jù)時間先后排序椭员,設(shè)置喚醒標(biāo)志车海。

可以喚醒時,代表有立即處理的消息隘击。

當(dāng)不滿足上述條件時侍芝,如果存在同步柵欄,個人理解是遇到 SyncBarrier 這個消息埋同,優(yōu)先處理后續(xù)的異步消息州叠,屏蔽同步消息,SyncBarrier 消息沒有 target 目標(biāo)凶赁,在 Android Framework 中是 hide 狀態(tài)咧栗,不會向 App 暴露逆甜。因此,上層應(yīng)用無此類消息致板,從源碼可知忆绰,當(dāng)隊(duì)列頭部遇到 SyncBarrier 消息,且插入消息是異步可岂,會主動喚醒错敢,除非隊(duì)列中已經(jīng)存在其他異步消息。

根據(jù)喚醒標(biāo)志缕粹,nativeWake() 方法稚茅,若不喚醒,只將消息插入到隊(duì)列合適位置平斩,線程保持休眠亚享。

void Looper::wake() {
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

Native 層篙程,wake() 方法智嚷,向 mWakeEventFd 句柄寫入內(nèi)容砰奕,(消息插入可能發(fā)生在其他任何線程)玻蝌。

Looper.loop() 消費(fèi)者線程在 MQ.next() 方法 epoll_wait() 位置休眠样悟,監(jiān)聽到 mWakeEventFd 事件仁讨,即注冊的流發(fā)生了事件绽乔,(寫入內(nèi)容非重點(diǎn))复哆,喚醒瘦馍。

awoken() 方法歼秽,數(shù)據(jù)流讀取,(內(nèi)容非重點(diǎn))情组,關(guān)鍵是線程被喚醒燥筷,繼續(xù)執(zhí)行,從 Native 層回到 Java 層 MQ.next() 位置 查找消息院崇。

四肆氓、總結(jié)擴(kuò)展

1,Native 層 Looper 類底瓣,利用 epoll 提供線程休眠和喚醒機(jī)制谢揪。

2,自動喚醒濒持,根據(jù)上層隊(duì)列對消息處理時間 when 的判斷键耕,決策喚醒 timeout 時間。

3柑营,主動喚醒屈雄,由 Handler 類主導(dǎo),消息隊(duì)列決策官套,向 epoll 監(jiān)聽的文件描述符寫入字段酒奶,觸發(fā)流事件蚁孔,即可喚醒。

3惋嚎,隊(duì)列按照消息處理時間升序排列杠氢。

4,Java 層功能另伍,消息創(chuàng)建鼻百、發(fā)送,隊(duì)列維護(hù)摆尝,喚醒時機(jī)温艇,消息處理。

擴(kuò)展堕汞,epoll 機(jī)制有兩種模式勺爱,ET 邊緣模式和 LT 水平模式 (默認(rèn))。
ET 模式讯检,狀態(tài)發(fā)生變化時才會事件通知琐鲁,例如,向流中寫入 10 個字節(jié)人灼,線程喚醒围段,僅讀取5個字節(jié),再次循環(huán)到此處時挡毅,不會再收到通知蒜撮,除非再次寫入句柄數(shù)據(jù),改變狀態(tài)跪呈。
LT 模式,有數(shù)據(jù)留在 Buffer 未讀取取逾,每次循環(huán)到此位置時耗绿,一直收到事件通知。


任重而道遠(yuǎn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砾隅,一起剝皮案震驚了整個濱河市误阻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晴埂,老刑警劉巖究反,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異儒洛,居然都是意外死亡精耐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門琅锻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卦停,“玉大人向胡,你說我怎么就攤上這事【辏” “怎么了僵芹?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長小槐。 經(jīng)常有香客問我拇派,道長,這世上最難降的妖魔是什么凿跳? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任件豌,我火速辦了婚禮,結(jié)果婚禮上拄显,老公的妹妹穿的比我還像新娘苟径。我一直安慰自己,他們只是感情好躬审,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布棘街。 她就那樣靜靜地躺著,像睡著了一般承边。 火紅的嫁衣襯著肌膚如雪遭殉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天博助,我揣著相機(jī)與錄音险污,去河邊找鬼。 笑死富岳,一個胖子當(dāng)著我的面吹牛蛔糯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窖式,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蚁飒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了萝喘?” 一聲冷哼從身側(cè)響起淮逻,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阁簸,沒想到半個月后爬早,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡启妹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年筛严,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翅溺。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡脑漫,死狀恐怖髓抑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情优幸,我是刑警寧澤吨拍,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站网杆,受9級特大地震影響羹饰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碳却,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一队秩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昼浦,春花似錦馍资、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至使兔,卻和暖如春建钥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虐沥。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工熊经, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人欲险。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓镐依,卻偏偏與公主長得像,于是被迫代替她去往敵國和親天试。 傳聞我的和親對象是個殘疾皇子馋吗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

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