移動架構 (二) Android 中 Handler 架構分析,並實現自己簡易版本 Handler 框架

DevYK發表於2019-07-18

Android 中訊息機制

Android 的訊息機制主要指 Handler 的執行機制,先來看下 Handler 的一張執行架構圖來對 Handler 有個大概的瞭解。

Handler 訊息機制圖:

Handler-.png

Handler 類圖:

Handler.png

以上圖的解釋:

  1. 以 Handler 的 sendMessage () 函式為例,當傳送一個 message 後,會將此訊息加入訊息佇列 MessageQueue 中。
  2. Looper 負責去遍歷訊息佇列並且將佇列中的訊息分發非對應的 Handler 進行處理。
  3. 在 Handler 的 handlerMessage 方法中處理該訊息,這就完成了一個訊息的傳送和處理過程。

這裡從圖中可以看到 Android 中 Handler 訊息機制最重要的四個物件分別為 Handler 、Message 、MessageQueue 、Looper。

ThreadLocal 的工作原理

ThreadLocal 是一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中儲存資料, 資料儲存以後,只有再指定執行緒中可以獲取到儲存的資料,對於其它執行緒來說則是無法獲取到儲存的物件。下面就是我們驗證 ThreadLocal 存取是否是按照剛剛那樣所說。

  • 子執行緒中存,子執行緒中取

     	// 程式碼測試       
     	new Thread("thread-1"){
                @Override
                public void run() {
                    ThreadLocal<String> mThread_A = new ThreadLocal();
                    mThread_A.set("thread-1");
                    System.out.println("mThread_A :"+mThread_A.get());
    
                }
            }.start();
    
    	//列印結果
    	mThread_A :thread-1
    複製程式碼
  • 主執行緒中存,子執行緒取

    	//主執行緒中存,子執行緒取    
    	final ThreadLocal<String> mThread_B = new ThreadLocal();   
    	mThread_B.set("thread_B");      
    	new Thread(){
                @Override
                public void run() {
                    System.out.println("mThread_B :"+mThread_B.get());
                }
            }.start();
    
    	//列印結果
    	mThread_B :null
    複製程式碼
  • 主執行緒存,主執行緒取

    	//主執行緒存,主執行緒取
            ThreadLocal<String> mThread_C = new ThreadLocal();
            mThread_C.set("thread_C");
            System.out.println("mThread_C :"+mThread_C.get());
    
    	//列印結果
    	mThread_C :thread_C
    複製程式碼

結果是不是跟上面我們所說的答案一樣,那麼為什麼會是這樣勒?現在我們帶著問題去看下 ThreadLocal 原始碼到底做了什麼?

ThreadLocal-.jpg

從上圖可以 ThreadLocal 主要函式組成部分,這裡我們用到了 set , get 那麼就從 set , get 入手吧。

ThreadLocal set(T):

ThreadLocal-set.jpg

​ (圖 1)

ThreadLocal-getMap.jpg

​ (圖 2)

ThreadLocal-createMap.jpg

​ (圖 3)

ThreadLocal-ThreadLocalMap-createMap-set.jpg

​ (圖 四)

從 (圖一) 得知 set 函式裡面獲取了當前執行緒,這裡我們主要看下 getMap(currentThread) 主要幹什麼了?

從 (圖二) 中我們得知 getMap 主要是從當前執行緒拿到 ThreadLocalMap 這個例項物件,如果當前執行緒的 ThreadLocalMap 為 NULL ,那麼就 createMap ,這裡的 ThreadLocalMap 可以暫時理解為一個集合物件就行了,它 (圖四) 底層是一個陣列實現的新增資料。

ThreadLocal T get():

ThreadLocal-get.jpg

這裡的 get() 函式其實已經能夠說明為什麼在不同執行緒儲存的資料拿不到了。因為儲存是在當前執行緒儲存的,取資料也是在當前所在的執行緒取得,所以不可能拿到的。帶著問題我們找到了答案。是不是有點小激動呀?(^▽^)

Android 訊息機制原始碼分析

這裡我們就直接看原始碼,一下是我看原始碼的流程。

  1. 建立全域性唯一的 Looper 物件和全域性唯一 MessageQueue 訊息物件。

    Handler--Looper-MessageQueue.png

  2. Activity 中建立 Handler。

    Handler-Activity-create.png

  3. Handler sendMessage 傳送一個訊息的走向。

    Handler-message-.png

  4. Handler 訊息處理。

    Handler-06c719af736b41fb.png

訊息阻塞和延時

阻塞和延時

Looper 的阻塞主要是靠 MessageQueue 來實現的,在 MessageQueue -> next() nativePollOnce(ptr, nextPollTimeoutMillis) 進行阻塞 , 在 MessageQueue -> enqueueMessage() -> nativeWake(mPtr) 進行喚醒。主要依賴 native 層的 looper epoll 進位制進行的。

f3da65a44123337f1b5b586a02aad8eb.png

阻塞和延時,主要是 next() 的 nativePollOnce(ptr , nextPollTimeoutMillis) 呼叫 native 方法來操作管道,由 nextPollTimeoutMillis 決定是否需要阻塞 , nextPollTimeoutMilis 為 0 的時候表示不阻塞 , 為 -1 的時候表示一直阻塞直到被喚醒,其它時間表示延時。

喚醒

主要是指 enqueueMessage () @MessageQueue 進行喚醒。

Handler-.jpg

阻塞 -> 喚醒 訊息切換

Handler-f6fe406dad09b444.jpg

總結

簡單的理解阻塞和喚醒就是在主執行緒的 MessageQueue 沒有訊息時,便阻塞在 Loop 的 queue.next() 中的 nativePollOnce() 方法裡面,此時主執行緒會釋放 CPU 資源進入休眠狀態,直到下一個訊息到達或者有訊息的時候才觸發,通過往 pipe 管道寫端寫入資料來喚醒主執行緒工作。

這裡採用的 epoll 機制,是一種 IO 多路複用機制,可以同時監控多個描述符,當某個描述符就緒 (讀或寫就緒) , 則立刻通知相應程式進行讀或者寫操作,本質同步 I/O , 即讀寫是阻塞的。所以說,主執行緒大多數時候都是處於休眠狀態,並不會消耗大量的 CPU 資源。

延時入隊

Handler-c4d53e3afdc11095.jpg

主要指 enqueueMessage() 訊息入佇列(Message 單連結串列),上圖程式碼對 message 物件池重新排序,遵循規則 ( when 從小到大) 。

此處 for 死迴圈退出情況分為兩種

  1. p == null 表示物件池中已經執行到了最後一個,無需要再迴圈。
  2. 碰到下一個訊息 when 小於前一個,立馬退出迴圈 (不管物件池中所有 message 是否遍歷完) 進行重新排序。

好了,到了這裡 Handler 原始碼分析算是告一段落了,下面我們來看下面試中容易被問起的問題。

常見問題分析

為什麼不能在子執行緒中更新 UI ,根本原因是什麼?

checkThread.jpg

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。

為什麼主執行緒用 Looper 死迴圈不會引發 ANR 異常?

簡單來說就是在主執行緒的 MessageQueue 沒有訊息時,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法,此時主執行緒會釋放 CPU 資源進入休眠狀態,直到下個訊息到達或者有事務發生,通過往 pipe 管道寫入資料來喚醒主執行緒工作。這裡採用的是 epoll 機制,是一種 IO 多路複用機制。

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

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

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

因為一個執行緒只繫結一個 Looper ,所以在 Looper 構造方法裡面初始化就可以保證 mQueue 也是唯一的 Thread 對應一個 Looper 對應一個 mQueue。

Handler . post 的邏輯在哪個執行緒執行的?是由 Looper 所線上程還是 Handler 所線上程決定的?

由 Looper 所線上程決定的。邏輯是在 Looper.loop() 方法中,從 MessageQueue 中拿出 message ,並且執行其邏輯,這裡在 Looper 中執行的,因此有 Looper 所線上程決定。

MessageQueue.next() 會因為發現了延遲訊息,而進行阻塞。那麼為什麼後面加入的非延遲訊息沒有被阻塞呢?

可以參考 訊息阻塞和延時 -> 喚醒

Handler 的 dispatchMessage () 分發訊息的處理流程?

handlerMessage-.jpg

  1. 屬於 Runnable 介面。

  2. 通過下面程式碼形式呼叫。

        private static Handler mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                return true;
            }
        });
    複製程式碼
  3. 如果第一步,第二部都不滿足直接走下面 handlerMessage 參考下面程式碼實現方式

        private static Handler mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
    複製程式碼

也可以通過 debug 方式來具體看 dispatchMessage 執行狀態。

實現自己的 Handler 簡單架構

主要實現測試程式碼

Handler-34a4a4e9e149d8c4.jpg

程式碼傳送陣

相關文章