Handler的原理——看這一篇就夠了

https://github.com/NieJianJian/AndroidNotes,內容將持續更新,歡迎star。


1.處理消息的手段——Handler、Looper與Message

​ 我們知道Android應用在啓動時,會默認有一個主線程(UI線程),在這個線程中會關聯一個消息隊列,所有的操作都會被封裝成消息然後交給主線程來處理。爲了保證主線程不會主動退出,會將獲取消息的操作放在一個死循環中,這樣程序就相當於一直在執行死循環,因此不會退出。

​ UI線程的消息循環是在ActivityThread.main方法中創建的,該函數爲Android應用程序的入。源代碼如下:

public static void main(String[] args){
    // ......
    Process.setArgsV0("<pre-initialized>");
    Looper.prepareMainLooper(); // 1.創建消息循環Looper
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null){
        sMainThreadHandler = thread.getHandler(); // UI線程的Handler
    }
    AsyncTask.init();
    // ......
    Looper.loop(); // 2.執行消息循環
    throw new RuntimeException("Main thread loop uexpectedly exited");    
}

​ 執行ActivityThread.main 方法後,應用程序就啓動了,並且會一直從消息隊列中取消息,然後處理消息,使得系統運轉起來。那麼系統是如何將消息投遞到消息隊列中的?又是如何從消息隊列中獲取消息並且處理消息的呢?答案就是Handler。

​ 在子線程中執行完耗時操作後,很多情況下需要更新UI,但我們都知道,不能在子線程中更新UI。此時最常用的手段就是通過Handler將一個消息post到UI線程中,然後再在Handler的handleMessage方法中進行處理。但是有一個點需要注意,那就是該Handler必須在主線程中創建,簡單示例如下:

class MyHandler extends Handler {
    @Override
    public void handleMessage(Message msg){
        // 更新UI
    }
}

MyHandler mHandler = new MyHandler();
// 開啓新線程
new Thread() {
    public void run() {
        // 耗時操作
        mHandler.sendEmptyMessage(123);    
    }    
}

​ 爲什麼必須要這麼做?其實每個Handler都會關聯一個消息隊列,消息隊列被封裝在Looper中,而每個Looper又會關聯一個線程(Looper通過ThreadLocal)封裝,最終就等於每個消息隊列會關聯一個線程。Handler就是一個消息處理器,將消息投遞給消息隊列,然後再由對應的線程從消息隊列中逐個取出消息,並且執行。默認情況下,消息隊列只有一個,即主線程的消息隊列,這個消息隊列是在ActivityThread.main方法中創建的,通過Looper.prepareMainLooper()來創建,最後執行Looper.loop()來啓動消息循環。那麼Handler是如何關聯消息隊列以及線程的呢?我們看看如下源代碼:

public Handler() {
    // ......
    mLooper = Looper.myLooper(); // 獲取Looper
    if (mLooper == null) {
        throw new RuntimeExcetion(
                "Can't create handler inside thread that has not called Looper.prepare()");
    }    
    mQueue = mLooper.mQueue; // 獲取消息隊列
    mCallback = null;    
}

​ 從Handler默認的構造函數中可以看到,Handler會在內部通過Looper.getLooper()來獲取Looper對象,並且與之關聯,最重要的就是消息隊列。那麼Looper.getLooper()又是如何工作的呢?我們繼續往下看:

public static Looper myLooper() {
    return sThreadLocal.get();
}

// 設置UI線程的Looper
public static void prepareMainLooper() {
    prepare();
    setMainLooper(myLooper());
    myLooper().mQueue.mQuitAllowed = false;
}

private synchronized static void setMainLooper(Looper looper) {
    sMainLooper = looper;    
}

// 爲當前線程設置一個Looper
public static void prepare() {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper());
}

​ 從上述程序中我們看到myLooper()方法是通過sThreadLocal.get()來獲取的。那麼Looper對象又是什麼時候存儲在sThreadLocal中的呢?眼見的朋友可能看到了,上面貼出的代碼中給出了一個熟悉的方法——prepareMainLooper(),在這個方法中調用prepare()方法,在這個prepare方法中創建了一個Looper對象,並且將該對象設置給了sThreaLocal。這樣,隊列就與線程關聯上了!這樣以來,不同的線程就不能訪問對方的消息隊列。

​ 再回到Handler中來,消息隊列通過Looper與線程關聯上,而Handler又與Looper關聯,因此,Handler最終就和線程、線程的消息隊列關聯上了。這樣就能解釋上面提到的問題,“爲什麼要更新UI的Handler必須要在主線程中創建?”。就是因爲Handler要與主線程的消息隊列關聯上,這樣handleMessage纔會執行在UI線程,此時更新UI纔是線程安全的!

​ 創建了Looper後,如何執行消息循環呢?通過Handler來post消息給消息隊列(鏈表),那麼消息是如何被處理的呢?答案就是在消息循環中,消息循環的建立就是通過Looper.loop()方法。源代碼如下:

public static void loop() {
    Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper;Looper.prepare wasn't called on this thread.");
    }
    MessageQueue queue = me.mQueue; // 1.獲取消息隊列
    // ......
    while (true) { // 2.死循環,即消息循環
        Message msg = queue.next(); // 3.獲取消息(might block)
        if (msg != null) {
            if (msg.target == null) {
                // No target is a magic identifier for the quit message。
                return;
            }
            // ......
            msg.target.dispatchMessage(msg); // 4.處理消息
            // ......
            msg.recycle();
        }
    }
}

​ 從上述程序中可以看到,loop方法中實質上就是建立一個死循環,然後通過從消息隊列中逐個取出消息,最後處理消息的過程。對於Looper我們總結一下:通過Looper.prepare()來創建Looper對象(消息隊列封裝在Looper對象中),並且保存在sThreadLocal中,然後通過Looper.loop()來執行消息循環,這兩步通常是成對出現的。

​ 最後,我們看看消息處理機制,我們看到上面的代碼中第4步通過msg.target.dispatchMessage(msg)來處理消息。其中msg是Message類型,我們看源代碼:

public final class Message implements Parcelable {
    Handler target; // target處理
    Runnable callback; // Runnable類型的callback
    Message next; // 下一條消息,消息隊列是鏈式存儲的
    // ......
}

​ 從源代碼中可以看到,target是Handler類型。實際上就是轉了一圈,通過Handler將消息投遞給消息隊列,消息隊列又將消息分發給Handler來處理。繼續看源代碼:

// 消息處理函數,子類覆蓋
public void handleMessage (Message msg) {
}

private final void handleCallback(Message message){
    message.callback.run();
}

// 分發消息
public void dispatchMessage (Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

​ 從上述程序中可以看到,dispatchMessage只是一個分發的方法,如果Runnable類型的callback爲空,則執行handleMessage來處理消息,該方法爲空,我們會將更新UI的代碼寫在該函數中;如果callback不爲空,則執行handleCallback來處理,該方法會調用callback的run方法。其實這是Handler分發的兩種類型,比如我們post(Runnable callback)則callback就不爲空,當我們使用Handler來sendMessage時通常不會設置callback,因此,也就執行handlerMessage這個分支。我們看看兩種實現:

public final boolean post(Runnable r){
    return sendMessageDelayed(getPostMessage(r), 0);
}

private final Message getPostMessage (Runnbale r){
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this; // 設置消息的target爲當前的Handler對象
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis); // 將消息插入到消息隊列
}

​ 從上述程序中可以看到,在post(Runnable r) 時,會將Runnable包裝成Message對象,並且將Runnable對象設置給Message對象的callback字段,最後會將該Message對象插入消息隊列。sendMessage也是類似實現。

public final boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

​ 不管是post一個Runnable還是send一個Message,都會調用sendMessageAtTime(msg, time)方法。Handler最終將消息追加到MessageQueue中,而Looper不斷地從MessageQueue中讀取消息,並且調用Handler的dispatchMessage方法,這樣消息就能源源不斷地被產生、添加到MessageQueue、被Handler處理。

2. MessageQueue如何處理消息

我們添加消息,最終會調用到MessageQueue的enqueueMessage方法:

boolean enqueueMessage(Message msg, long when) {
    ...
    synchronized (this) {
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 插入到queue
        }
        if (needWake) { nativeWake(mPtr); }
    }
    return true;
}

上述代碼可以看出,入隊的Message,如果滿足條件,則立即執行,如果不滿足,則加入隊列中。

處理消息則是調用MessageQueue的next方法取出消息進行處理:

Message next() {
    ...
    for (;;) {
        ...
        nativePollOnce(ptr, nextPollTimeoutMillis); // 1
        synchronized (this) {
            Message msg = mMessages;
            if (msg != null) { 
                if (now < msg.when) {
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, 
                            Integer.MAX_VALUE); // 2
                } else {
                    ...
                    return msg;
                }
            } else {
                nextPollTimeoutMillis = -1; // 3
            }
    ...
}

上述代碼可以看出,next也是通過死循環去取消息,

  • 先看註釋2處,如果msg不爲null,並且還不到執行的時間,則設置nextPollTimeoutMillis的值;
  • 如果msg不爲null,並且達到了執行的條件,則直接返回msg去執行;
  • 如果msg爲null,則將nextPollTimeoutMillis置爲-1;

接着我們看nativePoolOnce,會調用底層的NativeMessageQueue,假設該方法中發現nextPollTimeoutMillis值爲5000,則阻塞5000ms後自動返回,如果發現nextPollTimeoutMillis值爲-1,說明沒有消息需要處理,則會一直阻塞,那什麼時候喚醒呢?是在MessageQueue的enqueueMessage方法中,最後會判斷,如果neekWake爲true,就會調用底層的nativeWake方法,就會喚醒阻塞,也就是nativePoolOnce不再阻塞,返回後繼續往下執行代碼。底層採用的是epoll機制,用來監控描述符,如果描述符就緒,則會通知相應的程序進行讀寫。

3. 消息空閒IdleHandler

IdleHanlder是MessageQueue的內部類

/**
 * Callback interface for discovering when a thread is going to block
 * waiting for more messages.
 */
public static interface IdleHandler {
    /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    boolean queueIdle();
}

使用方式入如下:

MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            ...
            return false; 
        }
    };
Looper.myQueue().addIdleHandler(idleHandler);

在IdleHanlder的queueIdle()方法中,返回false表示一次性消費,執行完就移除掉;返回true表示這次執行完,下一次還會執行。

我們來看IdleHanlder的相關方法和內容:

public final class MessageQueue {
    ...
    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
    private IdleHandler[] mPendingIdleHandlers;

    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }

    public void removeIdleHandler(@NonNull IdleHandler handler) {
        synchronized (this) {
            mIdleHandlers.remove(handler);
        }
    }

具體的調用還是在MessageQueue的next方法中:

Message next() {
    ...
    for (;;) {
        ...
        nativePollOnce(ptr, nextPollTimeoutMillis); // 1
        synchronized (this) {
            Message msg = mMessages;
            if (msg != null) { 
                if (now < msg.when) {
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, 
                            Integer.MAX_VALUE); // 2
                } else {
                    ...
                    return msg;
                }
            } else {
                nextPollTimeoutMillis = -1; // 3
            }
            // 隊列空閒時,執行idle隊列
            if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                mBlocked = true;
                continue; // 空閒隊列爲null,則繼續執行for循環
            }
            if (mPendingIdleHandlers == null) {
               mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        } 
        // 運行空閒隊列
        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(); // 4
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }
            if (!keep) {  // 5
                synchronized (this) {
                    mIdleHandlers.remove(idler); // 6
                }
            }
        }
        // 將空閒隊列計數置爲0,這樣就不用再運行它們了。
        pendingIdleHandlerCount = 0;
        // 調用空閒隊列時,可能已經傳遞了一個新消息,所以返回重新執行一遍for循環
        nextPollTimeoutMillis = 0;
    } 
}

我們來總結一下空閒隊列的處理過程:

  • 如果next方法查詢到msg,則會直接return,去執行消息;
  • 如果當前隊列爲null,或者有消息還沒到執行的時間,就會繼續往下執行,運行到idle相關代碼;
  • 判斷存放idle的集合大小是否爲0,如果是則continue繼續執行for循環;
  • 如果idle集合不爲null,則idle集合轉換成數組去遍歷執行;
  • 每執行一個idleHandler任務,都會對queueIdle的返回值進行判斷,如果是false,就將此IdleHandler任務從idle集合中移除;
  • 遍歷執行完成後,將idle隊列計數置爲0 ,這樣就不用在運行它們了;
  • 還要將nextPollTimeoutMillis置爲0,因爲執行空閒隊列期間,可能有新的的Message創建,所以重新執行for循環去檢查新消息。

4. 在子線程中創建Handler爲何會拋出異常

​ 首先看代碼:

new Thread() {
    Handler handler = null;
    public void run(){
        handler = new Handler();    
    }
}.start();

​ 前面說過,Looper對象是ThreadLocal的,即每個線程都可以有自己的Looper,默認創建線程是沒有Looper的,這個Looper可以爲空。但是,當你要在子線程中創建Handler對象時,如果Looper爲空,那麼就會拋出"Can’t create handler inside thread that has not called Looper.prepare()"異常。爲什麼會這樣?看源代碼:

public Handler {
    // ......
    mLooper = Looper.myLooper(); // 獲取myLooper
    // 拋出異常
    if (mLooper == null) {
        throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = null;
}

​ 從上述程序中我們看到,當mLooper對象爲空時,拋出該異常。這是因爲該線程中的Looper對象還沒創建,因此sThreadLocal.get()會返回null。Handler的原理就是要與MessageQueue建立關聯,並且將消息投遞給MessageQueue,如果連MessageQueue都沒有,那麼mHandler就沒有存在的必要,而MessageQueue又被封裝在Looper中,因此,創建Handler時Looper一定不能爲空,解決辦法如下:

new Thread() {
    Handler handler = null;
    public void run(){
        Looper.prepare(); // 1.爲當前線程創建Looper,並且會綁定到ThreadLocal中
        handler = new Handler();
        Looper.loop(); // 2.啓動消息循環
    }
}.start();



問題1:爲什麼主線程的Looper.loop()死循環不會導致ANR

主線程的死循環一直運行是不是特別消耗CPU資源呢? 其實不然,這裏就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法裏,詳情見Android消息機制1-Handler(Java層),此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這裏採用的epoll機制,是一種IO多路複用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。 所以說,主線程大多數時候都是處於休眠狀態,並不會消耗大量CPU資源。

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

問題2:Handler導致內存泄露的路徑

Handler的聲明方式一般爲匿名內部類,這種情況可能會導致內存泄露。

  • 首先,匿名內部類會持有外部類的引用,也就是Handler會持有調用它的外部類的引用。

  • post或者send方法,最終都會調用到enqueueMessage方法中,方法中有如下一行代碼

    msg.target = this;
    

    設置Message的target字段爲當前的Handler對象。

  • 結合上面的步驟,如果Message處理消息,沒有處理完成,或者設置了delay,還沒到執行的時間,外部的Activity銷燬了,由於Handler持有Activity的引用,Message持有Handler的引用,Message會間接的持有外部Activity的引用,Message沒有處理完成,或者還沒來得及執行,是不會銷燬的,也導致外部Activity無法銷燬,從而導致了內存泄露。

  • 解決辦法

    • 關閉Activity的時候,調用Handler的removeCallbackAndMessage(null)方法,取消所有排隊的Message。
    • 將Handler創建爲靜態內部類,靜態內部類不會持有外部類的引用,並且使用WeakReference來包裝外部類對象。當Activity想關閉銷燬時,mHandler對它的弱引用沒有影響,該銷燬銷燬;當mHandler通過WeakReference拿不到Activity對象時,說明Activity已經銷燬了,就不用處理了,相當於丟棄了消息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章