大廠面試祕籍—— 深入理解 Handler

[外鏈圖片轉存失敗(img-G7pWn3TZ-1567932976355)(assets/16c0641a5f63bed0.png)]

概述

Android 的消息機制主要指的是 Handler 的運行機制,從開發者的角度來說 Handler 是 Android 消息機制的上層接口,而底層的邏輯則是由 MessageQueue、 Looper 來完成的。

Handler 的設計目的是爲了解決不能在 Android 主線程中做耗時操作而又只有主線程才能訪問 UI 的矛盾。通過 Handler 消息機制可以讓開發者在子線程中完成耗時操作的同時在主線程中更新UI

Handler 機制是 Android 用於 UI 刷新的一套消息機制。開發者可以使用這套機制達到線程間通信、線程切換目的。

這裏要思考一個問題:爲什麼 Android 非要規定只有主線程才能更新 UI 呢?

因爲 Android 的所有 View 控件都不是線程安全的,如果在多線程中併發訪問很可能造成意想不到的結果。對於加鎖這種方案也不可取,首先加鎖之後會讓 UI 訪問邏輯變的很複雜,開發者需要時刻考慮多線程併發將會帶來的問題,其次鎖機制太重了它會嚴重影響 UI 訪問效率。介於這兩個缺點,最簡單且高效的方法就是採用單線程的方式訪問 UI。Handler 機制應運而生。

那麼 Handler 內部是如何完成線程切換的呢?答案就是神奇的 :ThreadLocal

ThreadLocal

ThreadLocal 並不是 Thread ,他的特點很有意思: 每一個線程存儲的值是相互隔離的

public class TreadLocalDemo {
    // 就算設置爲 static 結果也是一樣的
    ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

    public void runDemo() {
        mThreadLocal.set(true);
        System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
        new Thread("Thread#1") {
            @Override
            public void run() {
                super.run();
                mThreadLocal.set(false);
                System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
            }
        }.start();

        new Thread("Thread#2") {
            @Override
            public void run() {
                super.run();
                System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
            }
        }.start();
        System.out.println(Thread.currentThread().getName() + "  " + mThreadLocal.get());
    }
}

運行的結果很清晰的展示他的特點,雖然在主線程和線程1中都做了賦值操作,但並不能改變原來線程的賦值情況。

file

對於 ThreadLocal 的原理簡單來說:每一線程都有一個專門用於保存 ThreadLocal 的成員變量 localValues。 儘管在不同線程中訪問同一個 ThreadLocal 的 setget 方法,但所做的操作都僅限制於各自線程的內部。這就是 ThreadLocal 可以在多個線程中互不干擾的存儲和讀取數據的原因。正是這種特性讓 Handler 做到了線程的切換。

Looper 正是藉助 ThreadLocal 的特點在不同的線程創建不同的實例。至此 Handler 與 Looper 、線程達到了一一對應的綁定關係。所以無論此 Handler 的實例在什麼線程調用,最終的回調都會分發到創建線程。

MessageQueue

MessageQueue 主要有兩個操作:插入和讀取。讀取操作也會伴隨着刪除。插入和讀取的方法分別對應的是:enquequeMessagenext,MessageQueue 並不是像名字一樣使用隊列作爲數據結構,而是使用單鏈表來維護消息。單鏈表在插入和刪除上比較有優勢。

next()

首先來說說 next 方法。

 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) {
            // 可見只有在調用 quit() 方法之後纔會返回空
            return null;
        } 
        
   ......
          
        // 一個死循環 !
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            // 一個 native 方法,此方法在沒有消息或者消息沒有到執行時間的時候會讓線程進入等待狀態。
          	// 有點類似於 Object.wait 但是 nativePollOnce 可以自定等待時間
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
   ......
       				 if (!keep) {
                    synchronized (this) {
                      // 獲取消息後從列表中移除
                        mIdleHandlers.remove(idler);
                    }
                }
        }
    }

最關鍵的是三點內容

  1. 死循環
  2. nativePollOnce()
  3. 獲取到消息之後從列表中移除

nativePollOnce 是一個 native 方法,如果單列表中沒有消息或者等待的時間沒有到,那麼當前線程將會被設置爲 **wait 等待狀態 **,直到可以獲取到下一個 Message更詳細的內容可以參見 StackOverflow 上關於 nativePollOnce的回答而這個死循環的目的就是不讓 next方法退出,等待 nativePollOnce 的響應。等到獲取到消息之後再將這個消息從消息列表中移除。

enqueueMessage()

enqueueMessage 方法的主要工作就是向單鏈表中插入數據,當線程處於等待狀態則調用 nativeWake 喚醒線程,讓 next 方法處理消息。

    boolean enqueueMessage(Message msg, long when) {
    ......
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

如何處理延時消息

詳情請參見Handler是怎麼做到消息延時發送的 下面再抄一部分結論:

在 next 方法中如果頭部的這個 Message 是有延遲而且延遲時間沒到的(now < msg.when),會計算一下時間(保存爲變量 nextPollTimeoutMillis), 然後在循環開始的時候判斷如果這個 Message 有延遲,就調用nativePollOnce (ptr, nextPollTimeoutMillis)進行阻塞。nativePollOnce()的作用類似與 Object.wait(), 只不過是使用了 Native 的方法對這個線程精確時間的喚醒。

  1. postDelay()一個10秒鐘的 Runnable A、消息進隊,MessageQueue 調用nativePollOnce()阻塞,Looper 阻塞;
  2. 緊接着 post() 一個 Runnable B、消息進隊,判斷現在A時間還沒到、正在阻塞,把B插入消息隊列的頭部(A的前面),然後調用 nativeWake()方法喚醒線程;
  3. MessageQueue.next() 方法被喚醒後,重新開始讀取消息鏈表,第一個消息B無延時,直接返回給 Looper
  4. Looper 處理完這個消息再次調用 next() 方法,MessageQueue 繼續讀取消息鏈表,第二個消息A還沒到時間,計算一下剩餘時間(假如還剩 9秒)繼續調用 nativePollOnce()阻塞;直到阻塞時間到或者下一次有Message 進隊;

file

Looper

Looper 在 Android 消息機制中扮演着消息循環的角色。具體來說他的任務就是不停的從 MessageQueue 中獲取消息,如果有新消息就立即處理,沒有消息的時候,與 Looper 綁定的線程就會被 MessageQueue 的 next 的 nativePollOne 方法置於等待狀態。

Looper 是如何創建的

/** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
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");
    }
    // 創建 Looper 實例,將實例保存在 sThreadLocal 中與當前線程綁定。
    sThreadLocal.set(new Looper(quitAllowed));
}

在構造方法裏面他會創建一個 MessageQueque,並保存當前線程。

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

getMainLooper 可以在任何地方獲取到主線程的 Looper,那麼主線程是如何創建 Looper 的呢?

主線程創建 Looper 的過程 —— AndroidThread

我們的目光來到了 AndroidThread 類, 在 AndroidThread 中我們看到了熟悉的方法 :main(String[] args。千萬不要被 AndroidThread 的名字所迷惑,AndroidThread 並不是一個線程,它只是一個開啓主線程的類。

public static void main(String[] args) {
        ....

        // 創建 Looper 和 MessageQueue 對象,用於處理主線程的消息
        Looper.prepareMainLooper();

        // 創建 ActivityThread 對象
        ActivityThread thread = new ActivityThread(); 

        // 建立 Binder 通道 (創建新線程)
        thread.attach(false);

 		// 消息循環運行
        Looper.loop(); 
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注意調用的是 prepare(false) 不允許退出,這是爲什麼呢?

    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 伴隨着一個 App 的整個生命週期,所有的 UI訪問、View 刷新都是在 Looper 裏面完成的,如果允許開發者手動退出,那麼整個 App 都會變得不可控。

更多細節可以參見下面的一節「 Looper中的死循環爲什麼沒有卡死線程」

Looper 是如何運行的

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    final Looper me = myLooper();
  ......
    
    final MessageQueue queue = me.mQueue;
  ......

    // 死循環
    for (;;) {
       // 可能會被阻塞
        Message msg = queue.next();
        if (msg == null) {
            // msg 爲 null 會立即退出循環,這也是退出循環的唯一方法。
            return;
        }
   ......
  
        try {
          // 開始分發消息
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
   ......
    }
}

loop 方法是一個死循環,他的工作就是不斷的檢查 MessageQueue 是否有可以處理的消息,如果有這將消息分發給 Handler 處理。既然是死循環那麼爲什麼沒有卡死線程呢?更多細節可以參見下面的一節「 Looper中的死循環爲什麼沒有卡死線程」

Looper 如何退出

Looper 內部提供了兩種退出的方法,分別是 quit、quitSafely。從本質上來講 quit 調用後會立即退出 Looper,而 quitSafely 只是設定一個退出標記,等待消息隊列中的已有消息處理完畢後,再退出。

Looper 退出後,通過 Handler 發送的消息會失敗,這個時候 Handler send 方法會返回 false。在子線程中,如果手動爲其創建了 Looper,那麼在所有的邏輯完成後理應手動調用 quit 方法終止 Looper 內部的循環,否則這個子線程會一直處於等待狀態,而退出 Looper 之後,子線程也就會隨之終止,因此在子線程中使用 Looper,必須在恰當的時機終止它

/**
* Quits the looper.
*/
public void quit() {
    mQueue.quit(false);
}

/**
 * Quits the looper safely.
 */
public void quitSafely() {
    mQueue.quit(true);
}

如果是主線程開發者就退出不了,要是退出了,就麻煩大了。

public static void prepareMainLooper() {
  // fasle 不允許退出
    prepare(false);
 ....
}

退出的本質

Looper.quit 的源碼中可以清晰看到,本質上調用的是 MessageQueue 的 quite 方法。而在調用 MessageQueue.quite 之後 再次調用 MessageQueue.next()會返回 null

 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) {
            // 可見只有在調用 quit() 方法之後纔會返回空
            return null;
        } 
        
   ......

Looper.loop()在調用 queue.next()得的結果爲 null 的時候會立即跳出死循環, 這也是退出死循環的唯一方式。

public static void loop() {
……
	for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }
……

Looper中的死循環爲什麼沒有卡死線程

參考知乎:Android中爲什麼主線程不會因爲Looper.loop()裏的死循環卡死?

我們都知道:一個簡單的死循環會消耗掉大量資源導致線程被卡死。但是 Looper.loop() 方法開啓就是一個死循環,它爲什麼沒有卡死線程呢?總結一下主要有3個疑惑:

  • Looper 爲什麼要使用死循環
  • Android 的主線程爲什麼沒有被 Looper 中的死循環卡死
  • 喚醒主線程 Looper 的消息從何而來

Looper 爲什麼要使用死循環

首先要說說的是爲什麼在 Looper 中使用死循環。在 CPU 看來操作系統線程(這裏的定義可以參見《Java基礎》多線程和線程同步 —— 進程與線程一節 ) 只不過是一段可以執行的代碼,CPU 會使用 CFS 調度算法,保證每一個 task 都儘可能公平的享用 CPU 時間片。既然操作系統線程是一段可以執行的代碼,當可執行的代碼結束之後,線程生命週期也就終止,線程將會退出。但是對於 Android 這類的 GUI 程序,我們絕對不希望代碼執行一段時間之後主線程就自己停止掉,那麼如何保證線程一直執行下去呢?簡單的做法就是在線程中執行死循環,讓線程一直工作下去不會停止退出。

總的來說,在線程中使用死循環想要解決的問題就是防止線程自己退出。所以對於 Looper 而言,他的死循環就是希望不斷的從 MessageQueue 中獲取消息,而不希望線程線性執行之後就退出。

Android 的主線程爲什麼沒有被 Looper 中的死循環卡死

首先 Android 所有的 UI 刷新和生命週期的回調都是由 Handler消息機制完成的,就是說 UI 刷新和生命週期的回調都是依賴 Looper 裏面的死循環完成的,這樣設計的目的上文已經闡述清楚。這篇文章裏面貼了 AndroidTread 對於 Handler 的實現類 H 的源碼(進入文章後搜索:內部類H的部分源碼) 源碼太長,我就不貼了。

其次Looper 不是一直拼命幹活的傻小子,而是一個有活就乾沒活睡覺的老司機,所以主線程的死循環並不是一直佔據着 CPU 的資源不釋放,不會造成過度消耗資源的問題。這裏涉及到了Linux pipe/epoll機制,簡單說就是在主線程的 MessageQueue 沒有消息時,便在 loop 的 queue.next() 中的 nativePollOnce() 方法裏讓線程進入休眠狀態,此時主線程會釋放CPU資源,直到下個消息到達或者有事務發生纔會再次被喚醒。所以 Looper 裏的死循環,沒有一直空輪詢着瞎忙,也不是進入阻塞狀態佔據着 CPU 不釋放,而是進入了會釋放資源的等待狀態,等待着被喚醒

經過上面的討論可以得知:

  1. Looper 中的死循環是 Android 主線程刷新 UI 和生命週期回調的基石。
  2. Looper 中的死循環會根據消息分別進入等待和喚醒狀態,並不會一直持有資源,所以就不會有卡死的問題。

那麼喚醒 Looper 的消息是從哪裏來的呢?

喚醒主線程 Looper 的消息從何而來

目光回到 AndroidThread 類中的這幾行代碼

public static void main(String[] args) {
        ....
          
        // 創建ActivityThread對象
        ActivityThread thread = new ActivityThread(); 
  
        //建立Binder通道 (創建新線程)
        thread.attach(false);

        Looper.loop(); //消息循環運行
    }

在創建 ActivityThread 後會通過thread.attach(false)方法在 ActivityThread 中創建 Binder 的服務端用於接收系統服務AMS發送來的事件,然後通過 ActivityThread 的內部類 ApplicationThread 中 sendMessage 方法

......

public final void scheduleStopActivity(IBinder token, boolean showWindow,
                int configChanges) {
           sendMessage(
                showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
                token, 0, configChanges);
        }

        public final void scheduleWindowVisibility(IBinder token, boolean showWindow) {
            sendMessage(
                showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW,
                token);
        }

        public final void scheduleSleeping(IBinder token, boolean sleeping) {
            sendMessage(H.SLEEPING, token, sleeping ? 1 : 0);
        }

        public final void scheduleResumeActivity(IBinder token, int processState,
                boolean isForward, Bundle resumeArgs) {
            updateProcessState(processState, false);
            sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0);
......

將消息發送給 AndroidThread 的 Handler 實現內部類 H。從而完成了 Binder Thread 到 UI 線程即主線程的切換,喚醒 Looper 進行 dispatchMessage 的動作。

喚醒的具體操作參見上文「MessageQueue -> enqueueMessage -> nativeWake」

拓展:如何在非主線程中使用 Handler 消息機制

通過 ActivityThread 的源碼可以清楚看到

public static void main(String[] args) {
        ....

        //創建Looper和MessageQueue對象,用於處理主線程的消息
        Looper.prepareMainLooper();
        ....

        Looper.loop(); //消息循環運行
        ....
    }

Android 在啓動一個 App 的時候都會創建一個 Looper,而用戶啓動子線程的時候是沒有這個操作的,所以需要開發者自己創建並調用 Looper.loop() 讓 Looper 運行起來。

   new Thread("Thread#1") {
      @Override
      public void run() {
         // 手動生成爲當前線程生成 Looper
         Looper.prepare();
         Handler handler = new Handler();
         Looper.loop();
      }

    }.start();

此處我們做個實驗,既然 Looper 是個死循環那麼在 loop() 之後的代碼是不是永遠沒有機會執行呢?

/**
 * Android 消息機制 —— Handler
 * <p>
 * Created by im_dsd on 2019-09-07
 */
public class HandlerDemo {

    public static final String TAG = "HandlerDemo";
    private Handler mHandler;
    private Looper mLooper;

    /**
     * 如何在子線程中開啓 Handler
     */
    public void startThreadHandler() {
        new Thread("Thread#1") {
            @Override
            public void run() {
                // 手動生成爲當前線程生成 Looper
                Looper.prepare();
                mLooper = Looper.myLooper();
                mHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        Log.d(TAG,Thread.currentThread().getName() + "  " + msg.what);
                    }
                };
                Log.d(TAG,Thread.currentThread().getName() + "loop 開始 會執行嗎?  ");
                // 手動開啓循環
                Looper.loop();
                Log.d(TAG,Thread.currentThread().getName() + "loop 結束 會執行嗎?  ");
            }

        }.start();

        // 等待線程啓動
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d(TAG,"start send message");
        mHandler.sendEmptyMessage(1);
        mHandler.post(() -> Log.d(TAG,Thread.currentThread().getName()));
    }
}

自啓動到將 App 徹底殺死,輸出結果也是如此:loop 後面的代碼沒有執行!

2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 開始 會執行嗎?  
2964-2964/com.example.dsd.demo D/HandlerDemo: start send message
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1  1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1

這意味兩個嚴重的問題:looper() 後面的代碼一直都不會執行而且線程 Thread#1 將會一直運行下去!在 JVM 規範裏面規定處於運行中的線程會不被 GC。在沒有消息的時候 Looper 會處於等待狀態。等待在 Thread 的生命週期裏仍然屬於運行狀態,它永遠不會被 GC

所以很多網上很多文章裏都有一個致命的缺陷,根本就沒有提及到要在使用完畢後即使退出 Looper。緊接上文的代碼

      // 嘗試 1秒 後停止
        try {
            Thread.sleep(1000);
            mLooper.quit();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

此時的結果

2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 開始 會執行嗎?  
2964-2964/com.example.dsd.demo D/HandlerDemo: start send message
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1  1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1
2964-3007/com.example.dsd.demo D/HandlerDemo: Thread#1loop 結束 會執行嗎?  

根據綜上所述,Handler 機制完全可以在 Android 中用作線程間的消息同步,這裏要強調一下,Handler 機制是 Android 獨有的,筆者在寫上面的 Demo 的時候竟然傻傻的將 Handler 的啓動放在了 Java 中,直接拋出了 RuntimException Stub 的錯誤。

總結一下在子線程中使用 Handler 機制要注意兩點問題:

  1. 必須調用 Looper.prepare();手動生成爲當前線程生成 Looper,並調用Looper.looper()啓動內部的死循環。
  2. 必須在使用結束後調用 Looper.myLooper().quit()退出當前線程。

Handler

Handler 的工作主要就是發送和接收消息。消息的發送可以通過 post 的一系列方法和 send 的一系類方法。在創建 Handler 的時候他會默認使用當前線程的 Looper ,如果當前線程沒有創建過 Looper 會拋出如下異常。

file

當然也可以手動指定不同線程的 Looper。

Handler mHandler = new Handler(Looper.getMainLooper());

消息是如何發送到的呢?

    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 final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
    }

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

經過一系列的跟蹤,最終的結果是調用了enqueueMessage(MessageQueue, Message, long)方法,目的就是爲了向 MessageQueue 中插入一條消息。

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

而後 nativeWake 將會喚醒等待的線程,MessageQueue#next將會在Looper.loop()中將這條消息返回,Looper.loop()在收到這條消息之後最終會交由 Handler#dispatchMessage處理

/** 
 * Looper 的 loop 方法
 */
public static void loop() {
  ......

    // 死循環
    for (;;) {
   ......
      // 開始分發消息  msg.target 指的就是發送消息的 Handler
       msg.target.dispatchMessage(msg);
   ......
    }
}
   /**
     * Handle 的 dispatchMessage 方法
     */
    public void dispatchMessage(Message msg) {
       // 首先檢查 msg 的 callback 是否爲 null
        if (msg.callback != null) {
          // 不爲 null 使用 msg 的 callback 處理消息
            handleCallback(msg);
        } else {
            // mCallback 是否爲 null
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
          // 都沒有指定則交由開發者重寫的 handleMessage 處理
            handleMessage(msg);
        }
    }

從上面的邏輯我們可以看出 callback 的優先級:msg#callback > new Handler(Callback) 中 指定的 Callback> 重寫 Handler 的 callBack

mCallback指的是一個接口 , 可以使用 Handler handler = new Handler(Callback)的方式指定回調,這種方式可以由外部傳遞進來會回調方法,更加靈活。

 * Callback interface you can use when instantiating a Handler to avoid
 * having to implement your own subclass of Handler.
 */
public interface Callback {
    /**
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public boolean handleMessage(Message msg);
}

至此對於 Android 的消息機制已經講解完畢,你是否已經有了清晰的認識呢?對於開篇的問題:Handler 是如何完成線程切換的,你找到答案了嗎?

常見問題分析

爲什麼不能在子線程中更新 UI ,根本原因是什麼?

16c0641ad370a90a.jpegfile

mThread 是主線程,這裏會檢查當前線程是否是主線程。

爲什麼 onCreate 裏面沒有進行上面的檢查呢?

這個問題原因出現在 Activity 的生命週期中 , 在 onCreate 方法中, UI 處於創建過程,對用戶來說界面還不可見,直到 onStart 方法後界面可見了,再到 onResume 方法後頁面可以交互,從某種程度來講, 在 onCreate 方法中不能算是更新 UI,只能說是配置 UI,或者是設置 UI 屬性。 這個時候不會調用到 ViewRootImpl.checkThread () , 因爲 ViewRootImpl 沒有創建。 而在 onResume 方法後, ViewRootImpl 才被創建。 這個時候去交戶界面纔算是更新 UI。

setContentView 知識建立了 View 樹,並沒有進行渲染工作 (其實真正的渲染工作實在 onResume 之後)。也正是建立了 View 樹,因此我們可以通過 findViewById() 來獲取到 View 對象,但是由於並沒有進行渲染視圖的工作,也就是沒有執行 ViewRootImpl.performTransversal。同樣 View 中也不會執行 onMeasure (), 如果在 onResume() 方法裏直接獲取 View.getHeight() / View.getWidth () 得到的結果總是 0 解決方案是在 UI 真正可見的方法 onWindowFocusChanged() 裏面獲取。

爲什麼 Handler 構造方法裏面的 Looper 不是直接 new ?

如果在 Handler 構造方法裏面直接 new Looper(), 可能是無法保證 Looper 唯一,只有用 Looper.prepare() 才能保證唯一性,具體可以看 prepare 方法。

MessageQueue 爲什麼要放在 Looper 私有構造方法初始化?

因爲一個線程只綁定一個 Looper ,所以在 Looper 構造方法裏面初始化就可以保證 mQueue 也是唯一的 Thread 對應一個 Looper 對應一個 mQueue。

總結

  1. Android 的消息機制指的就是 Handler 消息機制,Handler 是面向開發者的上層接口,而底層的實現是 MessageQueue、Looper、ThreadLocal
  2. MessageQueue 使用單鏈表的數據結構承載消息,在 next 方法中 nativePollOne 方法會在消息爲空的時候講線程置爲等待狀態,直到有新的消息到來纔會再次喚醒線程。所以 Looper.loop 雖然是死循環也不會卡死。
  3. Looper 的主要任務是不斷嘗試從 MessageQueue 中獲取消息,爲了不讓線程線性執行完畢,loop 中開啓了一個死循環。因爲主線程的所有生命週期都是由 Handler 機制完成的,所以這個主線程中死循環不允許開發者手動退出,什麼時候 App 退出了,這個死循環纔會被退出。而子線程中沒有這機制,所以在使用完畢後必須手動退出,否者線程會一直處於等待狀態,不會被GC。
  4. Handler 是藉助 ThreadLocal 機制完成線程切換的,Handler 在創建的時候就已經獲取了和線程綁定的 Looper,所以無論 Handler 在什麼線程調用,最終都會回到 Looper 綁定的線程,所以 Handler 很適合在 Android 中做線程間通信。

參考

  1. 《Android 開發藝術探究》第十章
  2. 知乎問答:《Android中爲什麼主線程不會因爲Looper.loop()裏的死循環卡死》
  3. StackOverflow: 《android - what is message queue native poll once in android?》
  4. 《Handler 是如何做到發送延時消息的》

Handler 的再理解與總結

MessageQueue#next()

這個方法裏面是一個死循環,但是裏面的方法 nativePollOnce 運用了 Linux 的 epoll 機制,在沒有消息的時候回會將線程掛起,注意此時的掛起相當於 Object.wait() : 它會釋放 CPU 資源,等待喚醒。有消息進入的時候回到用 MessageQueue#enqueueMessage() 加入數據,此時 MessageQueue#enqueueMessage() 內部的 nativeWeak 會重新喚醒線程,

可以發現

  1. 雖然是死循環但是他空閒時間並不消耗資源,死循環的目是爲了防止獲消息的邏輯退出
  2. **Loop#loop() 也是個死循環,但是沒有 message 的時候同樣會被 MessageQueue#next 掛起,不會控輪詢消耗資源 **。
  3. **當有消息進入的時候 next 方法會被立即喚醒,但是是否將消息返回不一定,要看是不是延時消息 **。

延時消息

MessageQueue#next() 中會判斷消息的時間,如果還沒有到消息執行的時間,會將消息定時掛起(我們記錄它 爲 A。如果這個時候有新的消息到來(記錄爲 B), MessageQueue#enqueueMessage() 會按照消息的執行時間排序插入,然後喚醒 MessageQueue#next() 處理消息。如果 B 不是定時消息立即處理,如果是定時消息更新掛起時間繼續阻塞,等到阻塞時間到的時候就會立即喚醒 next 方法處理。

爲什麼 MessageQueue#next() 需要死循環? loop 的死循環還不夠用嗎?

Loop#loop 裏面的死循環是爲了防止退出的,而 MessageQueue#next() 的死循環是爲了確認到底有沒有消息 參考:對於 MessageQueue 的解讀

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

        //爲-1時,說明是第一次循環,在當前消息隊列中沒有MSG的情況下,需要處理註冊的Handler
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        // 超時時間。即等待xxx毫秒後,該函數返回。如果值爲0,則無須等待立即返回。如果爲-1,則進入無限等待,直到有事件發生爲止
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {//???
                Binder.flushPendingCommands();
            }

            // 該函數提供阻塞操作。如果nextPollTimeoutMillis爲0,則該函數無須等待,立即返回。
            //如果爲-1,則進入無限等待,直到有事件發生爲止。
            //在第一次時,由於nextPollTimeoutMillis被初始化爲0,所以該函數會立即返回
            //從消息鏈的頭部獲取消息
            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) {//message不爲空,但沒有執行者
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {//尋找Asynchronous的消息
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        //判斷頭節點所代表的message執行的時間是否小於當前時間
                        //如果小於,讓loop()函數執行message分發過程。否則,需要讓線程再次等待(when–now)毫秒
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;//將隊列設置爲非 blocked 狀態
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        msg.markInUse(); //將消息設置爲 inuse
                        return msg;
                    }
                } else {
                //如果頭節點爲空,消息鏈中無消息,設置nextPollTimeoutMillis爲-1,讓線程阻塞住,
                //直到有消息投遞(調用enqueueMessage方法),並利用nativeWake方法解除阻塞
                    // 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)) {
                    //第一次進入,當前無消息,或還需要等待一段時間消息才能分發,獲得idle handler的數量
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    //如果沒有idle handler需要執行,阻塞線程進入下次循環
                    // 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("MessageQueue", "IdleHandler threw exception", t);
                }

                //如果keep=false,表明此idler只執行一次,把它從列表中刪除。如果返回true,則表示下次空閒時,會再次執行
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
        
            //pendingIdleHandlerCount=0,是爲了避免第二次循環時,再一次通知listeners
            //如果想剩餘的listeners再次被調用,只有等到下一次調用next()函數了
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // nextPollTimeoutMillis=0,是爲了避免在循環執行idler.queueIdle()時,有消息投遞。
            //所以nextPollTimeoutMillis=0後,第二次循環在執行nativePollOnce時,會立即返回
            //如果消息鏈中還是沒有消息,那麼將會在continue;處執行完第二次循環,進行第三次循環,然後進入無限等待狀態
            // 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;
        }
    }

重點:

  1. nativePollOnce(ptr, nextPollTimeoutMillis); nextPollTimeoutMillis: 0 不阻塞,-1 無限阻塞等待喚醒。
  2. 死循環最多執行三次:
    1. 第一次循環,如果消息鏈中有合適的消息,就拋出 message 去處理。如果沒有,則會通知各 listeners 線程空閒了。執行完後爲了避免在 listners 執行的過程中有消息投遞,那麼此時重置 nextPollTimeoutMillis = 0。
    2. 然後進行第二次循環,由於此時 nextPollTimeoutMillis 爲0,則 nativePollOnce 不會阻塞,立即返回,取出 message,如果此時消息鏈中還是沒有 message,則會在將會在 continue 處結束第二次循環,此時 nextPollTimeoutMillis 已被設置爲-1,
    3. 第三次循環時,nativePollOnce 發現 nextPollTimeoutMillis 爲-1,則進入無限等待狀態,直到有新的message 被投遞到隊列中來。當有新的 message 後,由於 enqueueMessage 中調用了 nativeWake 函數,nativePollOnce 會從等待中恢復回來並返回,繼續執行,然後將新的 message 拋出處理,for 循環結束。

enqueueMessage

enqueueMessage 有排序功能,按照時間入隊。

 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("MessageQueue", e.getMessage(), e);
                msg.recycle();//把這個消息放回到消息池中
                //獲得msg時,先去消息池中看看有沒有被回收的msg,如果有,就不用創建新的msg了
                return false;
            }

            msg.markInUse();
            msg.when = when;//從消息隊列中取出絕對時間戳
            Message p = mMessages;//指向隊首
            boolean needWake;
            //如果當前的消息鏈爲空,或者要插入的MSG爲QUIT消息,或者要插入的MSG時間小於消息鏈的第一個消息
            //在隊首插入
            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 {
                //否則,我們需要遍歷該消息鏈,將該MSG插入到合適的位置
                // 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;
            }

//neekWake=mBlocked, 如果mBlocked爲ture,表面當前線程處於阻塞狀態,即nativePollOnce處於阻塞狀態
//當通過enqueueMessage插入消息後,就要把狀態改爲非阻塞狀態,所以通過執行nativeWake方法,觸發nativePollOnce函數結束等待
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

本人最近在整理一套專屬 Android 進階筆記歡迎 start

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章