線程間通信:Handler機制

什么是Handler機制

Android系統(tǒng)不允許子線程訪問UI組件(子線程訪問主線程),主要是因為UI控件是非線程安全的,在多線程中并發(fā)訪問可能會導致UI控件處于不可預期的狀態(tài)。而不對UI控件的訪問加上鎖機制的原因有:

  • 上鎖會讓UI控件變得復雜和低效
  • 上鎖后會阻塞某些進程的執(zhí)行

而且Android系統(tǒng)為了避免ANR異常攒盈,通常需要新啟子線程來處理耗時操作,所以線程間的通信是很常見的開發(fā)場景。因此眼坏,為了解決子線程更新UI控件以及處理線程間的通信問題,系統(tǒng)提供了Handler機制酸些≡滓耄總的來說,Handler機制就是跨線程通信的消息傳遞機制魄懂。

簡介

Handler消息機制主要有四個核心類:Message(消息)沿侈、MessageQueue(消息隊列)、Looper(消息提取泵)市栗、Handler(消息處理者)缀拭。它們之間具體協(xié)作流程如下:

Handler運行流程

message(消息)

.Message的主要功能是進行消息的封裝咳短,同時可以指定消息的操作形式。我們類比生活中的郵寄信件來分析Message蛛淋,Message對象就是我們要寄出的信件咙好,它應該包括以下內(nèi)容:

收件人(消息發(fā)送給誰處理):

  • Handler target屬性:

①通過Message.setTarget(Handler target)來設置,我們常用的Handler.sendMessage(Message msg )發(fā)送消息時褐荷,在調(diào)用Handler類的enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)方法時勾效,也是設置Message對象的target屬性。

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

②在調(diào)用Messenger.send(Message message)發(fā)送消息時叛甫,Messenger對象內(nèi)部會封裝一個Handler對象葵第,這個handler對象就是Message的處理者。

    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    Messenger messenger = new Messenger(handler);
    messenger.send(message);

寄件人(對方怎么回信):

  • public Messenger replyTo屬性:
    發(fā)送消息時設置message.replyTo屬性合溺,在消息被接受以后卒密,接收者可以從消息對象的replyTo屬性提取出一個Messenger對象。通過這個Messenger對象就可以發(fā)送"回信"棠赛。當然你也可以發(fā)送一個不帶寄件人信息的匿名信件哮奇,所以replayTo不是必需設定的屬性。

信件內(nèi)容(要傳遞的信息或數(shù)據(jù))

  • Bundle data屬性:通過Bundle來封裝需要傳遞的數(shù)據(jù)睛约;
  • public Object obj屬性:
  • public int arg1屬性:如果只需要存儲幾個整型數(shù)據(jù)鼎俘,arg1 和 arg2是setData()的低成本替代品;
  • public int arg2屬性:如果只需要存儲幾個整型數(shù)據(jù)辩涝,arg1 和 arg2是setData()的低成本替代品贸伐。

信件ID(通過不同的ID,接收者做不同的業(yè)務處理)

  • public int what屬性:在設置what屬性的時候怔揩,需要注意不同Handler之間what值沖突捉邢。

其他屬性

  • Message next屬性:用來維護消息在消息隊列當中的順序(參見MessageQueue.enqueueMessage(Message msg, long when) 源碼);
  • Runnable callback屬性:設置這個屬性之后商膊,在消息處理的時候?qū)r截Handler.handleMessage(Message msg)伏伐,轉(zhuǎn)而執(zhí)行callback的run()方法(參見Handler.dispatchMessage(Message msg)源碼);
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

    private static void handleCallback(Message message) {
        message.callback.run();
    }

MessageQueue(消息隊列)

消息隊列被封裝到Looper里面了晕拆,我們一般不會直接與MessageQueue打交道藐翎。我們只需要記住它是用來存放消息的單鏈表結構。隊列的順序由Message的next屬性來維護实幕。MessageQueue是整個Handler機制的核心吝镣,里面涉及很多特性我們這里都不展開講述(比如消息屏障機制)±ケ樱可以擴展閱讀深入理解MessageQueue末贾、Handler之同步屏障機制(sync barrier)。這里我們只關心隊列的入列和出列凰锡。

    /**
     * MessageQueue.java
     * 往消息隊列添加消息的函數(shù)
     * @param msg  待添加的消息
     * @param when uptimeMillis(系統(tǒng)開機運行時間)+delayMillis(延遲時間未舟,由sendEmptyMessageDelayed設置)
     * @return
     */
    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                // 新建隊列頭圈暗,如果隊列為空掂为,或者有一個優(yōu)先級更高的消息插入到隊列頭部(譬如使用sendMessageAtTime(message,0))裕膀,
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                // 在消息隊列中間插入消息的情況。
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            // 這是一個native方法勇哗,實際作用就是通過Native層的MessageQueue阻塞nextPollTimeoutMillis毫秒的時間昼扛。
            // 1.如果nextPollTimeoutMillis=-1,一直阻塞不會超時欲诺。
            // 2.如果nextPollTimeoutMillis=0抄谐,不會阻塞,立即返回扰法。
            // 3.如果nextPollTimeoutMillis>0蛹含,最長阻塞nextPollTimeoutMillis毫秒(超時),如果期間有程序喚醒會立即返回塞颁。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        // Message設置的時間為到浦箱,將會阻塞等待。
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

由上面的代碼片段我們可以知道祠锣,在使用sendMessageAtTime(Message msg, long uptimeMillis)或者sendMessageDelayed(Message msg, long delayMillis)等方法的時候酷窥,我們傳入的uptimeMillis和delayMillis并不能準確的設置消息的處理時間。執(zhí)行的策略是:
①優(yōu)先執(zhí)行隊列中靠前的Message伴网;
②如果隊列中最考前的Message還沒準備好(SystemClock.uptimeMillis() < message.when)蓬推,此時會阻塞等待。

Looper(消息提取泵)

Looper的核心作用就是通過Looper.loop()不斷地從MessageQueue中抽取Message澡腾,并將消息分發(fā)給目標處理者沸伏。我們通過介紹Looper運行的三個步驟來掌握Looper的運行原理。

準備階段(Looper.prepare())
我們知道整個消息處理機制的運作动分,就是將消息添加進消息隊列以及從消息隊列提前消息進行分發(fā)馋评。Looper.prepare()就是用來初始化創(chuàng)建Looper對象和消息列表對象的函數(shù)。因此刺啦,Looper.prepare()是我們使用Handler機制時必不可少的第一步(注意我們平時在使用主線程的handler時留特,都是直接new Handler(),具體原因我們稍后分析)玛瘸。

    /**
     * Looper.java
     */
    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));
    }

由上面的源碼可以發(fā)現(xiàn)蜕青,每個線程只能創(chuàng)建一個Looper(每個Looper又只有一個消息隊列),但是消息隊列里面可以存放多個Message糊渊,而每個Message都可以通過.setTarget(Handler target)來設置自己的Handler處理者右核。這就是Handler機制中四大要素之間的數(shù)理關系。

  • prepareMainLooper():順帶說一下這個方法渺绒,它的作用就是在UI線程(主線程)調(diào)用Looper.prepare()贺喝,并且會保留一個主線程Looper對象的靜態(tài)引用菱鸥。這個引用可以通過Looper.getMainLooper()來獲取主線程Looper,方便跟子線程跟主線程通信躏鱼。

啟動和運行Looper.loop()

    /**
     * Looper.java
     */
    public static void loop() {
        // 獲取當前線程的Looper對象
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 獲取當前線程的消息隊列
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            // 死循環(huán)不斷的從消息隊列中提取消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                // 根據(jù)MessageQueue里面的next()方法得知氮采,要使得queue.next()返回null的情況只有兩種
                // 1:MessageQueue.mPtr == 0;這里的mPtr值是調(diào)用native方法創(chuàng)建MessageQueue之后,返回的MessageQueue的引用地址染苛,通過這種方式將java層的對象與Native層的對象關聯(lián)在一起
                //MessageQueue.mPtr == 0說明消息隊列創(chuàng)建失斎的;
                // 2:MessageQueue.mQuitting == true;也就是調(diào)用Looper的quit()/quitSafely()方法之后茶行,next方法會返回null
                return;
            }

            //.........省略部分代碼..........
            try {
                // 調(diào)用Handler的dispatchMessage(Message msg)方法處理消息
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            //.........省略部分代碼..........

            msg.recycleUnchecked();
        }
    }

根據(jù)源碼(配合上面的MessageQueue.next()方法的源碼服用)躯概,我們可以清晰的看出來Looper.loop()方法的業(yè)務邏輯就是一個死循環(huán)不停的用MessageQueue.next()方法從消息隊列中提取消息,并將消息交給target所持有的Handler進行處理(調(diào)用Handler.dispatchMessage(msg)方法)畔师。

  • 因為這里是死循環(huán)娶靡,所以線程的run()方法中,一般情況下Looper.loop()調(diào)用之后的代碼邏輯不會被執(zhí)行到(特殊情況看锉,請參考下面的死循環(huán)退出條件)
    private class MyThread extends Thread {
        private Looper subLooper;
        
        @Override
        public void run() {
            Looper.prepare();
            subLooper = Looper.myLooper();
            initHandler();
            Looper.loop();

            System.out.println("這句話永遠不會被打印姿锭,除非消息隊列初始化失敗或者調(diào)用了Looper.quit()");
        }
    }
  • 這里的死循環(huán)其實是有退出條件的MessageQueue.next()返回null的時候會return,循環(huán)終止度陆。循環(huán)終止的情況有兩種:
    ①MessageQueue.mPtr == 0;這里的mPtr值是調(diào)用native方法創(chuàng)建MessageQueue之后艾凯,返回的MessageQueue的引用地址,通過這種方式將java層的對象與Native層的對象關聯(lián)在一起懂傀。MessageQueue.mPtr == 0說明消息隊列創(chuàng)建失斨菏;
    ②2:MessageQueue.mQuitting == true;也就是調(diào)用Looper的quit()/quitSafely()方法之后蹬蚁,next方法會返回null恃泪。
  • 如果MessageQueue為空,消息列表沒有消息犀斋,將會阻塞等待贝乎。

終止和退出quit()/quitSafely()

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市叽粹,隨后出現(xiàn)的幾起案子览效,更是在濱河造成了極大的恐慌,老刑警劉巖虫几,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锤灿,死亡現(xiàn)場離奇詭異辆脸,居然都是意外死亡但校,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門术裸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匹表,“玉大人冻晤,你說我怎么就攤上這事「宕妫” “怎么了冕臭?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵连霉,是天一觀的道長。 經(jīng)常有香客問我,道長戈鲁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任嘹叫,我火速辦了婚禮婆殿,結果婚禮上,老公的妹妹穿的比我還像新娘罩扇。我一直安慰自己婆芦,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布暮蹂。 她就那樣靜靜地躺著寞缝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仰泻。 梳的紋絲不亂的頭發(fā)上荆陆,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音集侯,去河邊找鬼被啼。 笑死,一個胖子當著我的面吹牛棠枉,可吹牛的內(nèi)容都是我干的浓体。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼辈讶,長吁一口氣:“原來是場噩夢啊……” “哼命浴!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤生闲,失蹤者是張志新(化名)和其女友劉穎媳溺,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碍讯,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡悬蔽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捉兴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝎困。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倍啥,靈堂內(nèi)的尸體忽然破棺而出禾乘,到底是詐尸還是另有隱情,我是刑警寧澤逗栽,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布盖袭,位于F島的核電站失暂,受9級特大地震影響彼宠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弟塞,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一凭峡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧决记,春花似錦摧冀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扩借,卻和暖如春椒惨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背潮罪。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工康谆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫉到。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓沃暗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親何恶。 傳聞我的和親對象是個殘疾皇子孽锥,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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