再讀Handler機制

一、引言

距離上一次閱讀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機制的核心內容,也可以說是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進行分發。

至此,是不是有些明白爲什麼不用查找效率更高的數組或查找樹來設計了呢?首先是額外內存損耗的問題,單向鏈表的空間複雜度肯定比模板庫中的隊列(需要額外建立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莫屬了。

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

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

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

總結

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

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