Android Handler消息機制

導讀

由于我更新優化本篇文章中的筆誤之處,導致文章莫名被刪除,故此重新發布!

Android Handler消息機制

為了避免ANR,我們通常會把耗時操作放在子線程里面去執行,因為子線程不能更新UI,所以當子線程需要更新UI的時候就需要借助到Andriod的消息機制,也就是Handler機制了。那么其原理是什么呢?下面我們根據平時的使用一步一步來解讀源碼。
以上內容可能會產生的問題:

  • 什么是ANR?

    在Android上,如果你的應用程序有一段時間無法響應,系統會向用戶顯示一個對話框,這個對話框稱作應用程序無響應(ANR:Application Not Responding)對話框。用戶可以選擇讓程序繼續運行,但是,他們在使用你的應用程序時,并不希望每次都要處理這個對話框。因此,在程序里對響應性能的設計很重要,這樣,系統不會顯示ANR給用戶。

  • 耗時操作為什么不能再主線程?
  • 為什么子線程不能更新UI?
  • Handler機制是什么?

一. Handler的常見用法。

  1. 創建handler對象并重寫handleMessage方法,以便于處理自己需要的邏輯。
private Handler handler = new Handler(){
  @Override
  public void handleMessage(Message msg){
    ...
  }
}
  1. 使用handler 對象發送消息(對象、載體)。
private void sendMsg(){
  Mssages msg = handler.obtainMessage();
  msg.what=what;
  msg.age1=age1;
  handler.sendMessageDelayed(msg,1000);
}
...

二. 代碼分析

  1. 以上代碼可以看出,在主線程創建了一個handler對象,并重構了handleMessage方法,然后通過handler發送消息對象,中間經過一系列處理后,handleMessage方法接收到傳遞的數據,就可以處理具體的邏輯了(如更新UI)。
  2. 以上代碼可以看出,發送消息、最后處理數據時都使用的是Message對象,這是在源碼設計層面時約定的媒介(載體、中間件、快遞),這里先不進行展開,只做一個簡單的介紹:

what字段作為標識,
arg1/arg2字段作為簡單類型的參數,
obj字段作為復雜對象,以及Bundle常見參數
target字段作為handler對象標識

  1. 再來看發送消息 sendMessageDelayed,通過源碼可以發現所有的sendMessage方法執行后,最終都會走sendMessageAtTime()方法(這里就不貼上具體源碼了)。
   public boolean sendMessageAtTime(@NonNull 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);
    }

在這個sendMessageAtTime方法里會拿到一個MessageQueue的實例對象,并觸發enqueueMessage(queue, msg, uptimeMillis)方法。那我們就先看看enqueueMessage方法,然后在去看mQueue怎么來的。

      boolean enqueueMessage(Message msg, long when) {
        ...
        Message p = mMessages;
        synchronized (this) {
         if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
            ...  
            Message p = mMessages;
            ...
            msg.next = p; // invariant: p == prev.next
            ...
        }
      }
        return true;
    }

首先這是一個被synchronized修飾的同步代碼塊,確保了并發問題;核心邏輯是對Message的實例對象p進行非空判斷,最終兩個兩個邏輯里都會把msg放到Message實例對象p的next方法中進行排隊。

4.現在回來研究這個mQueue對象是怎么來的呢,通過command+f(ctrl+f)搜索,最終發現是Handler構造方法中獲取的(通過代碼可以看出mQueue的獲取是通過looper.mQueue,而我們前面在創建handler的時候使用的是無參構造的,那么這個Looper對象的實例額mLooper是怎么來的?

 @UnsupportedAppUsage
    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

4.1 觀察Handler的其他重載的構造方法發現,我們的無參構造最后會走如下構造,而這個mLooper實例對象是通過Looper.myLooper()方法獲取的而且還不能為空!由此可以看出Looper、MessageQueue都是非常重要的對象。

   public Handler(@Nullable Callback callback, boolean async) {
       ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
    }

4.2 繼續跟蹤源碼,進入Looper類發現myLooper()的內部是通過sThreadLocal.get()獲取的,那么有get()自然是有set()的,所以我們command+f(ctrl+f)搜索找到了prepare()方法,而這個方法會被prepareMainLooper的方法執行,prepare翻譯過來是準備的意思,prepareMainLooper就是準備主Looper的意思了,這里要注意有synchronized修飾噢。

   ...
   public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
   ...
   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();
        }
    }

4.3 到這里,我們知道了Handler無參構造的時候其實是使用了一個叫MainLooper及MessageQueue的對象,那這個MainLooper具體又是在哪里創建的呢,那就查查這個prepareMainLooper()方法在哪里被調用的使用command+shift+f進行全局搜索:


WX20200201-115959@2x.png

搜索出來4個結果,除了Looper.java外,SystemServer.java是系統級別的這里不做深入,后續新增該類的解讀; Bridge.java是橋梁類,注釋中寫著“Main entry point of the LayoutLib Bridge.”,這里也不做深入;最后是ActivityThread.java類,也是這里需要重點關注的類:

public final class ActivityThread extends ClientTransactionHandler {
  ...
   public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
        ...
    }
  ...
}

有兩個重大發現:
1、Looper.prepareMainLooper()是在一個叫ActivityThread 的 final 類型的Java類,的main(String[] args) 方法中執行的。
2、還執行了Looper.loop();
main是Java的入口方法,說明什么呢,在整個(應用)代碼里Looper.prepareMainLooper()是最先執行的代碼之一,也就是說Handler所使用的Looper早就初始化好了,而且是同步唯一的(如果忘了請回翻 )。

接下來看看loop()方法

 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;
        ...
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ...
            msg.target.dispatchMessage(msg);
            ...
            msg.recycleUnchecked();
        }
    }

這里可以看出幾個重要消息
1、Looper、MessageQueue是不可缺少的對象
2、使用到了for (;;) ,就是說無限循環,一直去MessageQueue的next()去查有沒有Message對象,
3、有Message對象時會執行msg.target.dispatchMessage(msg);方法
當然也會有2個困惑的問題
3.1、for (;;) 這個無限循環不會讓APP卡死嗎?
3.2、 有一行代碼if (msg == null) return 其注釋是的意思是是阻塞,return之后不是繼續for循環嗎?

4.4 繼續跟蹤 msg.target.dispatchMessage(msg)方法,前面有提到target其實就是Handler(為什么呢,大家有興趣可以看看源碼)也就是說dispatchMessage方法是Handler類下的

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

發現這里調用了一個很眼熟的方法名handleMessage 沒錯,這就是一開始我們重寫的的handleMessage方法,至此,腦海中是否已經有了一個Handle整體輪廓圖?是否還有一個疑問,Handler啥時候把線程給切換了?

總結

1.APP在啟動的時候,最先啟動ActivityThread類的main方法,其中Looper的初始化工作和準備工作就是這時候執行的(獲得Looper對象、MessageQueue)
2.Looper開啟loop()方法(永動機)去死循環遍歷MessageQueue的消息隊列(至于為什么沒有卡死前面有提到?有提到嗎,為什么我修改的時候發現沒有,捂臉)
3.在通常用法中創建Handler對象時,構造方法會拿到looper、mQueue對象
4.使用Handler對象發送消息(sendXxx())
5.把消息加入隊列(mQueue.enqueueMessage)
6.第2步的永動機讀取MessageQueue的消息
7.執行dispatchMessage方法
8.執行代碼中創建Handler方法時重寫的handleMessage方法完成Handler機制的整個過程。

最后,讓我們帶著文章中的問題,進行繼續深入:Android中為什么主線程不會因為Looper.loop里的無限循環ANR?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容