細致一點地看看 Handler 和它的伙伴們

Handler 對于 Android 開發(fā)來說簡直就是家常便飯忿檩,它的原理自然都很熟悉,這篇文章不會宏觀地去介紹它的原理爆阶,而是細節(jié)深入到各個組成燥透。

目錄

關(guān)系

開始深入細節(jié)的時候,我們可以先復習下 Handler 辨图、Looper 和 MessageQueue 三者的關(guān)系班套。

  1. Handler 必須在 Looper.prepare() 之后才能創(chuàng)建使用
  2. Looper 與當前線程關(guān)聯(lián),并且管理著一個 MessageQueue
  3. Message 是實現(xiàn) Parcelable 接口的類
  4. 以一個線程為基準故河,他們的數(shù)量級關(guān)系是:
    Handler(N) : Looper(1) : MessageQueue(1) : Thread(1)

他們的調(diào)用關(guān)系可以參考這張圖:


分析

0x01
public Handler(Callback callback, boolean async) {
    // 代碼省略

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

從 Handler 默認的構(gòu)造函數(shù)我們可以看到吱韭,Handler 內(nèi)部會通過 Looper.myLooper() 來獲取 Looper 對象,從而與之關(guān)聯(lián)鱼的。

0x02

我們之前已經(jīng)知道 Looper 管理著消息隊列理盆,從這里深入進去看看是如何跟 MessageQueue 建立聯(lián)系瞻讽。

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

public static void prepare() {
    prepare(true);
}

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));
}

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

Looper.myLooper() 里我們看到,Looper 是通過 sThreadLocal.get() 來獲取熏挎,那么我們又是何時將 Looper 設(shè)置給 sThreadLocal 的呢速勇?答案就在 prepare() 方法里。
我們看到 sThreadLocal.set(new Looper(quitAllowed)); 實例化了一個 Looper 對象給 sThreadLocal 并且一個線程只有一個 Looper 坎拐。

同時我也貼出了 prepareMainLooper() 方法烦磁,根據(jù)名字大家都可以猜到,這個方法就是在 Android 主線程(UI)線程調(diào)用的方法哼勇,而在這個方法里也調(diào)用了 prepare(false) 我們看到這里傳入的是 false 都伪,表明主線程這里的 Looper 是無法執(zhí)行 quit() 方法。
我在這里貼出 ActivityThread 的 Main() 方法的部分代碼积担,這也是我們程序的入口:

public static void main(String[] args) {
    // 代碼省略

    Looper.prepareMainLooper(); // 創(chuàng)建消息循環(huán) Looper

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler(); // UI 線程的 Handler
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    Looper.loop(); // 執(zhí)行消息循環(huán)

}

在這里我們更清楚了為什么可以直接在主線程創(chuàng)建 Handler 陨晶,而不會發(fā)生異常。

以上帝璧,我們明白了 Looper 是通過 prepare() 方法與線程建立聯(lián)系先誉,同時不同線程是無法訪問對方的消息隊列。

為什么 Handler 要在主線程創(chuàng)建才能更新 UI 呢的烁?

因為 Handler 要與主線程的消息隊列關(guān)聯(lián)上褐耳,這樣 handleMessage() 才會執(zhí)行在 UI 線程。

0x03

Looper 的核心其實是它循環(huán)取出消息的代碼:

public static void loop() {
    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;

    // 死循環(huán)
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        /// Handler msg.target
        msg.target.dispatchMessage(msg); // 派發(fā)消息

        // 代碼省略

        msg.recycleUnchecked();
    }
}

從上面代碼我們可以看到渴庆,Looper 在 loop() 方法里建立了一個死循環(huán)铃芦,通過消息隊列里不斷的取出消息,交給 Handler 去處理襟雷。

這個時候你可能會有一個問題:

Android 中為什么主線程不會因為 Looper.loop() 里的死循環(huán)卡死刃滓?

我比較推薦 Gityuan 的回答

回到我們這里,在循環(huán)中是通過 msg.target.dispatchMessage(msg); 派發(fā)消息耸弄。其中 msg 是 Message 類型咧虎,簡單看看它的成員:

public final class Message implements Parcelable {
    Handler target;
    Runnable callback;
    Message next;

    public Object obj;
    public int arg1;
    public int arg2;

    // 代碼省略
}

可以知道消息隊列是鏈表實現(xiàn)的,并且 target 是 Handler 類型叙赚。

現(xiàn)在就可以連通了老客,通過 Handler 將 Message 投遞給消息隊列(鏈表)僚饭,Looper.loop() 循環(huán)從消息隊列里取出消息震叮,又將消息分發(fā)給 Handler 去處理。通過這個 target 我們也可以知道一個小細節(jié)鳍鸵,Handler 只能處理自己所發(fā)出的消息苇瓣。

0x04

理解清楚之后我們跟著順序,看看 Handler 是如何處理和分發(fā)消息的偿乖。

// 處理消息方法击罪,交給子類復寫
public void handleMessage(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();
}

我們看到 dispatchMessage() 只是一個分發(fā)方法哲嘲,如果 Runnable 類型的 callback 為空,則執(zhí)行 handleMessage(msg) 處理信息媳禁,該方法為空眠副,是交給子類進行復寫,并且執(zhí)行線程是在 Handler 所創(chuàng)建的線程竣稽。
如果 callback 不為空囱怕,則會執(zhí)行 handleCallback(msg) 來處理信息,該方法會調(diào)用 callback 的 run() 方法毫别。

其實說簡單一點娃弓,就是 Handler 的兩種分發(fā)類型。
一種是 post(r) 另一種是 sendMessage(msg)岛宦。

我們具體看看這兩個方法:

public final boolean post(Runnable r){
   return  sendMessageDelayed(getPostMessage(r), 0);
}

private static Message getPostMessage(Runnable r){
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

public final boolean sendMessage(Message msg){
    return sendMessageDelayed(msg, 0);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis){
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this; // 與當前 Handler 綁定
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

做了一個導圖台丛,方便理解下:


從中我們可以看到,在 post(r) 時砾肺,會將 Runnable 包裝成 Message 對象挽霉,并且賦值給 Message 的 callback 字段,最后跟 sendMessage(msg) 方法一樣將消息插入隊列变汪。

根據(jù)代碼和導圖炼吴,無論是 post(r) 還是 sendMessage(msg) 都會最終調(diào)用 sendMessageAtTime(msg,time)

總結(jié)

Handler 最終將消息追加到 MessageQueue 中,而 Looper 不斷的從 MessageQueue 中讀取消息疫衩,并且調(diào)用 Handler 的 dispatchMessage 分發(fā)消息硅蹦,最后交給上層處理消息。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闷煤,一起剝皮案震驚了整個濱河市童芹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鲤拿,老刑警劉巖假褪,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異近顷,居然都是意外死亡生音,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門窒升,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缀遍,“玉大人,你說我怎么就攤上這事饱须∮虼迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長譬挚。 經(jīng)常有香客問我锅铅,道長,這世上最難降的妖魔是什么减宣? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任盐须,我火速辦了婚禮,結(jié)果婚禮上漆腌,老公的妹妹穿的比我還像新娘丰歌。我一直安慰自己,他們只是感情好屉凯,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布立帖。 她就那樣靜靜地躺著,像睡著了一般悠砚。 火紅的嫁衣襯著肌膚如雪晓勇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天灌旧,我揣著相機與錄音绑咱,去河邊找鬼。 笑死枢泰,一個胖子當著我的面吹牛描融,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衡蚂,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼窿克,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了毛甲?” 一聲冷哼從身側(cè)響起年叮,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玻募,沒想到半個月后只损,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡七咧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年跃惫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艾栋。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡爆存,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出裹粤,到底是詐尸還是另有隱情终蒂,我是刑警寧澤遥诉,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布拇泣,位于F島的核電站,受9級特大地震影響矮锈,放射性物質(zhì)發(fā)生泄漏霉翔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一债朵、第九天 我趴在偏房一處隱蔽的房頂上張望瀑凝。 院中可真熱鬧粤咪,春花似錦、人聲如沸宪塔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽某筐。三九已至,卻和暖如春冠跷,著一層夾襖步出監(jiān)牢的瞬間南誊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工蜜托, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弟疆,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓盗冷,卻偏偏與公主長得像怠苔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子仪糖,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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