再讀Handler機制

horseLai發表於2019-08-20

一、引言

距離上一次閱讀Handler原始碼已經半年多過去了,當時讀原始碼的目的更多的是忙於畢業找工作,為面試做準備,現在則是想更多地瞭解Android相關機制。半年多過去了,回頭看當時的原始碼解讀筆記(當時不寫部落格),發現有很多地方並沒有很好地解釋清楚,於是想趁著這2018結束之際再次根據自己的想法整理一遍,感興趣的童鞋可以看看。

所謂Handler機制,實際是執行緒切換機制。在我們日常開發中用的最多的是通過Handler來更新UI檢視,而Handler除了用於執行緒切換外,HandlerLooperThreadLocalMessageQueueMessage如何融合、構成一個成熟框架的思想更值得我們學習,這也是寫本文的目的:比較深入全面地理解Handler機制。

二、預熱知識

通常,我們使用Handler是這樣的:

private Handler mHandler1;

 mHandler1 = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            // 操作 UI
            textView.setText(String.valueOf(msg.obj));
            return false;
        }
    });  
複製程式碼

又如這樣手動建立一個訊息迴圈:

Handler mHandler;
Looper mLooper;
private void createHandlerLoop() {
   Thread looperThread =  new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            mLooper = Looper.myLooper();
            mHandler = new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    // 操作UI時異常
                    // Looper不執行於UI執行緒,不能直接操作View控制元件
                    // textView.setText(String.valueOf(msg.obj));
                    Toast.makeText(MainActivity.this, String.valueOf(msg.obj), Toast.LENGTH_SHORT).show();
                    return true;
                }
            });
            Looper.loop();
        }
    });
    looperThread.start();
}
複製程式碼

然後我們給兩個Handler傳送訊息,這裡我們從非主執行緒傳送訊息:

private ExecutorService mExecutorService = Executors.newCachedThreadPool();
public void sendMessage(View view) {
    mExecutorService.submit(new Runnable() {
        @Override
        public void run() {
            if (mHandler != null) 
                mHandler.obtainMessage(1, "" + Math.random()).sendToTarget(); 
            if (mHandler1 != null) 
                mHandler1.obtainMessage(1, "`from mHandler1::" + Math.random()).sendToTarget();
        }
    }); 
}
複製程式碼

我們知道Handler執行於Looper,而Looper執行於建立它的執行緒中,因此可以說Handler執行於建立它的執行緒,這裡剛好驗證一下,我們給mHandler1mHandler發訊息,然後在其中更新UI,此時mHandler1正常更新,而mHandler則報非主執行緒更新UI的異常,可見 mHandler1Looper執行於主執行緒,而mHander則執行於looperThread

當然,這些只是表象上,我們來看看為啥它執行在建立它的執行緒。

首先來看看Handler的構造方法

public Handler``(Callback callback, boolean async) {
    // ...
    mLooper = Looper.myLooper();
    // ...
} 
複製程式碼

這裡主要關注到Looper.myLooper(),它是從ThreadLocal中獲取到的,而之所以能得到,是因為在執行Looper.prepare()方法時會建立一個Looper例項存入其中,這也就是為什麼我們在手動建立訊息迴圈時必須要執行Looper.prepare()的原因了。

// 
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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));
} 
複製程式碼

注意到上面sThreadLocalstatic final所修飾,也就是說它以常量的形式存在於程式中,所以我們應用程式中的所有Looper例項都會獨立儲存在其中,無論執行在主執行緒還是其他執行緒。ThreadLocal#get()原始碼如下:

public T get() {
    // 1.  拿到當前執行緒
    Thread t = Thread.currentThread();
    // 2.  取出當前執行緒的獨立資料
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 3. 取出執行在當前執行緒的 Looper 例項
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
複製程式碼

可以看出,從ThreadLocal取出Looper例項時會根據當前執行的執行緒去取,也就是說在Handler執行於的執行緒決定於Looper所執行於的執行緒,也就是建立它的,並執行了Looper#prepare()的執行緒。

好了,接下來將結合原始碼分析HandlerLooperThreadLocalMessageQueueMessage的具體運作原理。

三、 Looper 原始碼分析

Looper物件建立時會建立一個訊息佇列,並記錄當前執行緒資訊

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
複製程式碼

而實際上呼叫Looper.prepare()時才會真正意義上的建立Looper物件,Looper物件會記錄在ThreadLocal中,而ThreadLocal在這裡是個static final常量,意味著會在執行時以常量形式記錄在常量池,從而保證使用到Looper的執行緒都只對應這唯一的ThreadLocal常量,也就保證不同使用到Looper訊息迴圈的執行緒中都對應著唯一的Looper,且相互之間是獨立的。

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));
}
複製程式碼

loop()方法則是個死迴圈,所以執行之後,會使得建立它的執行緒就處於阻塞狀態,此時會一直處於從MessageQueue中提取Message的狀態,直到呼叫Looper.quit()方法退出訊息佇列。Looper.quit()執行後使得queue.next()返回Messagenull,Looper也隨即退出,執行緒最終執行結束。

在訊息迴圈正常執行過程中,會將每個從訊息佇列中提取到的訊息分發給handler#dispatchMessage(msg)處理,這裡的msg.target就是Message記錄下來的傳送它的那個Handler

public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue; 
    // . . .
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {  // 拿到的訊息為null了,說明訊息佇列已經退出了,因而退出訊息迴圈
            return;
        }

        try { // 分發訊息給handler處理
            msg.target.dispatchMessage(msg); 
        } finally { 
            // ...
        } 
    }
}
複製程式碼

執行mQueue.quit(false)後訊息佇列會刪除佇列內的所有訊息,並把根節點Message設定為nullMessageQueue是個單向連結串列結構,後面會講到),所以上面如果queue.next()拿到的訊息為null了,就說明訊息佇列已經退出,因而就會退出Looper#loop(),也就是退出訊息迴圈。

public void quit() {
    mQueue.quit(false);
}
void quit(boolean safe) {
    // ...
    synchronized (this) {
        // ... 
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        } 
        // We can assume mPtr != 0 because mQuitting was previously false.
        nativeWake(mPtr);
    }
}
private void removeAllMessagesLocked() {
    Message p = mMessages;
    while (p != null) {
        Message n = p.next;
        p.recycleUnchecked();
        p = n;
    }
    mMessages = null;
}
複製程式碼

至於Handler.dispatchMessage(),原始碼如下,可見它首先會看看是否給Message設定了回撥,如果有,那執行Message的回撥方法,如果沒有,則看看自己的回撥,如果沒有再就執行自己的方法;所以Message自身的回撥是優先順序最高的,其次是自身回撥,最後才是自身的方法。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
複製程式碼

以上整個分析流程下來就可以得出如下草圖,其中包含了執行緒切換的概念在內:

Handler機制.png

以上便是Handler機制的核心內容,也可以說是Looper的執行流程,當然這還沒完,因為我們在分析過程中依然存在其他疑問,比如以單向連結串列結構為基礎的Messagequque為啥這麼設計、ThreadLocal為啥能夠獨立儲存執行緒資料等。

三、MessageQueue 原始碼分析

訊息佇列顧名思義是用來存放Message的佇列,但實際上它不是使用佇列,而是以Message為節點的單連結串列結構;它提供enqueueMessage(Message msg, long when)方法用來插入訊息,next()來提取訊息,它是一個阻塞方法,只有當Looper.quit呼叫後它才會退出。

這裡重點分析一下它的next()方法,它採用了執行緒安全設計,並且它也是阻塞式的,我們先來看看原始碼:

Message next() {
    // ...
    for (;;) {
        // ...
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 1. 遍歷單向連結串列,也就是訊息佇列,查詢尾部節點
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }

            if (msg != null) {
                // 2. 根據訊息的執行時間判定是否要返回、處理這條訊息
                if (now < msg.when) {
                    // 還沒輪到它
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else { // 時間到,就是它了
                    mBlocked = false;
                    if (prevMsg != null) {  // 刪除隊尾節點
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    // 3. 這條訊息可以處理了,則標記為正在使用並返回
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            // 檢查訊息迴圈是否退出
            if (mQuitting) {
                dispose();
                return null;
            }
            // 其他情況,則繼續迴圈等待
        }
        // ...
    }
}
複製程式碼

由於是基於單向連結串列的設計,因而每次如果想要得到隊尾節點都需要遍歷整個佇列,直到定位到隊尾。一旦定位到了隊尾,且它不為null,那麼就會根據訊息的執行時間判定是否要返回、處理這條訊息,如果還沒到點,那麼繼續迴圈(也可看成是在等待,如果是非同步isAsynchronous的,那麼就直接使用這個訊息,而不需要定位到隊尾),直到達到了執行時間,而達到執行時間後,說明這條訊息可以處理了,則標記為已用,並返回給Looper進行分發。

MessageQueue#next.png

至此,是不是有些明白為什麼不用查詢效率更高的陣列或查詢樹來設計了呢?首先是額外記憶體損耗的問題,單向連結串列的空間複雜度肯定比模板庫中的佇列(需要額外建立Entry)要低;然後是這裡的執行時間等待問題,實際這裡並不需要查詢時間複雜度上的最優,因為反正都可能需要等待執行時間,那麼遍歷就完事了,多耗點時間也無所謂。

再來看看插入訊息,與取訊息類似,執行緒安全設計,具體看看註釋就行。

boolean enqueueMessage(Message msg, long when) {
    // . . . 
    synchronized (this) {
        if (mQuitting) {  // quit()方法呼叫後,訊息迴圈已經退出
            msg.recycle();
            return false;
        }
        // 標記為正在使用
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // 如果沒到執行時間,則直接插在隊頭
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else { // 其他則插入到隊尾,比如已經到了執行時間了,那麼直接插入到隊頭,這樣就能在下次取訊息時執行了
            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;
        }
        // ...
    }
    return true;
}
複製程式碼

四、ThreadLocal 原始碼分析

特點: 當多個執行緒使用同一個ThreadLocal時,它可以獨立地記錄各個執行緒的資料。

ThreadLocal#set(...)原始碼如下,可見每當執行ThreadLocal#set(...)儲存執行緒資料時,會先檢視當前執行緒是否已經繫結了ThreadLocalMap資料集合,如果不存在則建立一個繫結在這個執行緒。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製程式碼

再來看看ThreadLocalMap的主要構成,看到這樣的構成是不是可以聯想到HashTableHashMap呢?當然,不過不同之處在於,它並沒有像HashTable那樣採用陣列+單向連結串列的設計,也沒有像HashMap那樣採用陣列+單向連結串列+紅黑樹的設計,而僅僅是一個陣列,不過原理都是類似的。

也就是說,實際上ThreadLocal本身並不儲存資料,而ThreadLocal之所以能夠實現單獨記錄每個使用到它的執行緒的資料,是因為它為每一個執行緒都建立了一個獨立的ThreadLocalMap資料儲存物件,並把這個資料儲存物件繫結在對應的執行緒上,當我們需要設定或者獲取某一個執行緒的資料時,那麼只需要從這個執行緒中取出這個資料儲存物件,然後寫/讀資料即可,你看這是不是既簡便又能保證資料的獨立性。

static class ThreadLocalMap { 
    // 真正執行緒儲存資料的地方
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value; 
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    } 
    private Entry[] table;  
    private int size = 0; 
    private int threshold; // Default to 0
    // ...
}
複製程式碼

ThreadLocal#get()原始碼如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //1. 取出當前執行緒所繫結的的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //2. 取出 ThreadLocalMap.Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;   //3. 取出值
            return result;
        }
    }
    return setInitialValue();
}
複製程式碼

五、設計思想

至此,我們已經結合原始碼分析了Handler機制中所有核心,可能有的童鞋會有所懷疑,Handler機制的核心居然不是Handler?哈哈,確實不是,Handler只是作為整個框架的入口而已,要說核心,那非Looper莫屬了。

縱觀整個機制的執行過程及協作關係,不難發現它實際是基於生產者-消費者模型的設計:

生產者-消費者.png

為了方便對比,我把前面的Handler機制那張圖粘過來這裡:

Handler機制.png

然後對號入座一下,不難發現,這裡的生產者指的是Thread 2,任務佇列就是我們的MessageQueue,而消費者就是Looper也就是Thread 1,然後你會發現,Handler還真的只是個入口。

總結

本文從Handler的基本使用著手,結合原始碼分析了Handler機制中的幾個核心類,並總結和下它的設計思想,水平有限,歡迎指正。

相關文章