【Android面試查漏補缺】之Handler詳解,帶你全面理解Handler消息機制

在安卓面試中,關於 Handler 的問題是必備的,可是這些關於 Handler 的知識點你都知道嗎?java

1、題目層次

  1. Handler 的基本原理
  2. 子線程中怎麼使用 Handler
  3. MessageQueue 獲取消息是怎麼等待
  4. 爲何不用 wait 而用 epoll 呢?
  5. 線程和 Handler Looper MessageQueue 的關係
  6. 多個線程給 MessageQueue 發消息,如何保證線程安全
  7. Handler 消息延遲是怎麼處理的
  8. View.post 和 Handler.post 的區別
  9. Handler 致使的內存泄漏
  10. 非 UI 線程真的不能操做 View 嗎

2、題目詳解

代碼分析基於 Android SDK 28android

你們能夠先看上面的問題思考一下,若是都清楚的話,下面的文章也不必看了~面試

1. Handler 的基本原理

關於 Handler 的原理,相比不用多說了,你們都應該知道,一張圖就能夠說明(圖片來自網絡)。segmentfault

2. 子線程中怎麼使用 Handler

除了上面 Handler 的基本原理,子線程中如何使用 Handler 也是一個常見的問題。 子線程中使用 Handler 須要先執行兩個操做:Looper.prepare 和 Looper.loop。 爲何須要這樣作呢?Looper.prepare 和 Looper.loop 都作了什麼事情呢? 咱們知道若是在子線程中直接建立一個 Handler 的話,會報以下的錯誤:安全

"Can't create handler inside thread xxx that has not called Looper.prepare()

咱們能夠看一下 Handler 的構造函數,裏面會對 Looper 進行判斷,若是經過 ThreadLocal 獲取的 Looper 爲空,則報上面的錯誤。網絡

public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
    }

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

那麼 Looper.prepare 裏作了什麼事情呢?app

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

能夠看到,Looper.prepare 就是建立了 Looper 並設置給 ThreadLocal,這裏的一個細節是每一個 Thread 只能有一個 Looper,不然也會拋出異常。 而 Looper.loop 就是開始讀取 MessageQueue 中的消息,進行執行了。socket

這裏通常會引伸一個問題,就是主線程中爲何不用手動調用這兩個方法呢?相信你們也都明白,就是 ActivityThread.main 中已經進行了調用。 經過這個問題,又能夠引伸到 ActivityThread 相關的知識,這裏就不細說了。async

3. MessageQueue 如何等待消息

上面說到 Looper.loop 其實就是開始讀取 MessageQueue 中的消息了,那 MessageQueue 中沒有消息的時候,Looper 在作什麼呢?咱們知道是在等待消息,那是怎麼等待的呢?ide

經過 Looper.loop 方法,咱們知道是 MessageQueue.next() 來獲取消息的,若是沒有消息,那就會阻塞在這裏,MessageQueue.next 是怎麼等待的呢?

public static void loop() {
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        }
    }
Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            // ...
        }
    }

在 MessageQueue.next 裏調用了 native 方法 nativePollOnce。

// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    // ...
    mLooper->pollOnce(timeoutMillis);
    // ...
}

// Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    // ...
    result = pollInner(timeoutMillis);
    // ...
}

int Looper::pollInner(int timeoutMillis) {
    // ...
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
}

從上面代碼中咱們能夠看到,在 native 側,最終是使用了 epoll_wait 來進行等待的。 這裏的 epoll_wait 是 Linux 中 epoll 機制中的一環,關於 epoll 機制這裏就不進行過多介紹了,你們有興趣能夠參考 http://www.javashuo.com/article/p-sqagmtjn-x.html

那其實說到這裏,又有一個問題,爲何不用 java 中的 wait / notify 而是要用 native 的 epoll 機制呢?

4. 爲何不用 wait 而用 epoll 呢?

提及來 java 中的 wait / notify 也能實現阻塞等待消息的功能,在 Android 2.2 及之前,也確實是這樣作的。 能夠參考這個 commit https://www.androidos.net.cn/android/2.1_r2.1p2/xref/frameworks/base/core/java/android/os/MessageQueue.java 那爲何後面要改爲使用 epoll 呢?經過看 commit 記錄,是須要處理 native 側的事件,因此只使用 java 的 wait / notify 就不夠用了。 具體的改動就是這個 commit https://android.googlesource.com/platform/frameworks/base/+/fa9e7c05c7be6891a6cf85a11dc635a6e6853078%5E%21/#F0

Sketch of Native input for MessageQueue / Looper / ViewRoot

MessageQueue now uses a socket for internal signalling, and is prepared
to also handle any number of event input pipes, once the plumbing is
set up with ViewRoot / Looper to tell it about them as appropriate.

Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3

不過這裏最開始使用的仍是 select,後面才改爲 epoll。 具體可見這個 commit https://android.googlesource.com/platform/frameworks/base/+/46b9ac0ae2162309774a7478cd9d4e578747bfc2%5E%21/#F16

至於 select 和 epoll 的區別,這裏也不細說了,你們能夠在上面的參考文章中一塊兒看看。

5. 線程和 Handler Looper MessageQueue 的關係

這裏的關係是一個線程對應一個 Looper 對應一個 MessageQueue 對應多個 Handler。

6. 多個線程給 MessageQueue 發消息,如何保證線程安全

既然一個線程對應一個 MessageQueue,那多個線程給 MessageQueue 發消息時是如何保證線程安全的呢? 說來簡單,就是加了個鎖而已。

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
    }
}

7. Handler 消息延遲是怎麼處理的

Handler 引伸的另外一個問題就是延遲消息在 Handler 中是怎麼處理的?定時器仍是其餘方法? 這裏咱們先從事件發起開始看起:

// Handler.java
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    // 傳入的 time 是 uptimeMillis + delayMillis
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    // ...
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    // 調用 MessageQueue.enqueueMessage
    return queue.enqueueMessage(msg, uptimeMillis);
}

從上面的代碼邏輯來看,Handler post 消息之後,一直調用到 MessageQueue.enqueueMessage 裏,其中最重要的一步操做就是傳入的時間是 uptimeMillis + delayMillis。

boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
        msg.when = when;
        Message p = mMessages; // 下一條消息
        // 根據 when 進行順序排序,將消息插入到其中
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 找到 合適的節點
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            // 插入操做
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // 喚醒隊列進行取消息
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

經過上面代碼咱們看到,post 一個延遲消息時,在 MessageQueue 中會根據 when 的時長進行一個順序排序。 接着咱們再看看怎麼使用 when 的。

Message next() {
    // ...
    for (;;) {
        // 經過 epoll_wait 等待消息,等待 nextPollTimeoutMillis 時長
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // 當前時間
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 得到一個有效的消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) { // 說明須要延遲執行,經過; nativePollOnce 的 timeout 來進行延遲
                    // 獲取須要等待執行的時間
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else { // 當即執行的消息,直接返回
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                        // 當前沒有消息要執行,則執行 IdleHandler 中的內容
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 若是沒有 IdleHandler 須要執行,則去等待 消息的執行
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 執行 idle handlers 內容
        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(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // 若是執行了 idle handlers 的內容,如今消息可能已經到了執行時間,因此這個時候就不等待了,再去檢查一下消息是否能夠執行, nextPollTimeoutMillis 須要置爲 0
        nextPollTimeoutMillis = 0;
    }
}

經過上面的代碼分析,咱們知道了執行 Handler.postDelayd 時候,會執行下面幾個步驟:

  1. 將咱們傳入的延遲時間轉化成距離開機時間的毫秒數
  2. MessageQueue 中根據上一步轉化的時間進行順序排序
  3. 在 MessageQueue.next 獲取消息時,對比當前時間(now)和第一步轉化的時間(when),若是 now < when,則經過 epoll_wait 的 timeout 進行等待
  4. 若是該消息須要等待,會進行 idel handlers 的執行,執行完之後會再去檢查此消息是否能夠執行

8. View.post 和 Handler.post 的區別

咱們最經常使用的 Handler 功能就是 Handler.post,除此以外,還有 View.post 也常常會用到,那麼這兩個有什麼區別呢? 咱們先看下 View.post 的代碼。

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

經過代碼來看,若是 AttachInfo 不爲空,則經過 handler 去執行,若是 handler 爲空,則經過 RunQueue 去執行。 那咱們先看看這裏的 AttachInfo 是什麼。 這個就須要追溯到 ViewRootImpl 的流程裏了,咱們先看下面這段代碼。

// ViewRootImpl.java
final ViewRootHandler mHandler = new ViewRootHandler();

public ViewRootImpl(Context context, Display display) {
    // ...
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
}

private void performTraversals() {
    final View host = mView;
    // ...
    if (mFirst) {
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mFirst = false;
    }
    // ...
}

代碼寫了一些關鍵部分,在 ViewRootImpl 構造函數裏,建立了 mAttachInfo,而後在 performTraversals 裏,若是 mFirst 爲 true,則調用 host.dispatchAttachedToWindow,這裏的 host 就是 DecorView,若是有讀者朋友對這裏不太清楚,能夠看看前面【面試官帶你學安卓-從View的繪製流程】提及這篇文章複習一下。

這裏還有一個知識點就是 mAttachInfo 中的 mHandler 實際上是 ViewRootImpl 內部的 ViewRootHandler。

而後就調用到了 DecorView.dispatchAttachedToWindow,其實就是 ViewGroup 的 dispatchAttachedToWindow,通常 ViewGroup 中相關的方法,都是去依次調用 child 的對應方法,這個也不例外,依次調用子 View 的 dispatchAttachedToWindow,把 AttachInfo 傳進去,在 子 View 中給 mAttachInfo 賦值。

// ViewGroup
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    for (int i = 0; i < transientCount; ++i) {
        View view = mTransientViews.get(i);
        view.dispatchAttachedToWindow(info,
                combineVisibility(visibility, view.getVisibility()));
    }
}

// View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...
}

看到這裏,你們可能忘記咱們開始剛剛要作什麼了。

咱們是在看 View.post 的流程,再回顧一下 View.post 的代碼:

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    getRunQueue().post(action);
    return true;
}

如今咱們知道 attachInfo 是什麼了,是 ViewRootImpl 首次觸發 performTraversals 傳進來的,也就是觸發 performTraversals 以後,View.post 都是經過 ViewRootImpl 內部的 Handler 進行處理的。

若是在 performTraversals 以前或者 mAttachInfo 置爲空之後進行執行,則經過 RunQueue 進行處理。

那咱們再看看 getRunQueue().post(action); 作了些什麼事情。

這裏的 RunQueue 實際上是 HandlerActionQueue。

HandlerActionQueue 的代碼看一下。

public class HandlerActionQueue {
    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }

    public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }
}

經過上面的代碼咱們能夠看到,執行 getRunQueue().post(action); 實際上是將代碼添加到 mActions 進行保存,而後在 executeActions 的時候進行執行。

executeActions 執行的時機只有一個,就是在 dispatchAttachedToWindow(AttachInfo info, int visibility) 裏面調用的。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

看到這裏咱們就知道了,View.post 和 Handler.post 的區別就是:

  1. 若是在 performTraversals 前調用 View.post,則會將消息進行保存,以後在 dispatchAttachedToWindow 的時候經過 ViewRootImpl 中的 Handler 進行調用。
  2. 若是在 performTraversals 之後調用 View.post,則直接經過 ViewRootImpl 中的 Handler 進行調用。

這裏咱們又能夠回答一個問題了,就是爲何 View.post 裏能夠拿到 View 的寬高信息呢? 由於 View.post 的 Runnable 執行的時候,已經執行過 performTraversals 了,也就是 View 的 measure layout draw 方法都執行過了,天然能夠獲取到 View 的寬高信息了。

9. Handler 致使的內存泄漏

這個問題就是老生常談了,能夠由此再引伸出內存泄漏的知識點,好比:如何排查內存泄漏,如何避免內存泄漏等等。

10. 非 UI 線程真的不能操做 View 嗎

咱們使用 Handler 最多的一個場景就是在非主線程經過 Handler 去操做 主線程的 View。 那麼非 UI 線程真的不能操做 View 嗎? 咱們在執行 UI 操做的時候,都會調用到 ViewRootImpl 裏,以 requestLayout 爲例,在 requestLayout 裏會經過 checkThread 進行線程的檢查。

// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

咱們看這裏的檢查,其實並非檢查主線程,是檢查 mThread != Thread.currentThread,而 mThread 指的是 ViewRootImpl 建立的線程。 因此非 UI 線程確實不能操做 View,可是檢查的是建立的線程是不是當前線程,由於 ViewRootImpl 建立是在主線程建立的,因此在非主線程操做 UI 過不了這裏的檢查。

3、總結

一個小小的 Handler,其實能夠引伸出不少問題,這裏這是列舉了一些你們可能忽略的問題,更多的問題就等待你們去探索了~ 這裏來總結一下:

1. Handler 的基本原理

一張圖解釋(圖片來自網絡) handler

2. 子線程中怎麼使用 Handler

  1. Looper.prepare 建立 Looper 並添加到 ThreadLocal 中
  2. Looper.loop 啓動 Looper 的循環

3. MessageQueue 獲取消息是怎麼等待

經過 epoll 機制進行等待和喚醒。

4. 爲何不用 wait 而用 epoll 呢?

在 Android 2.2 及以前,使用 Java wait / notify 進行等待,在 2.3 之後,使用 epoll 機制,爲了能夠同時處理 native 側的消息。

5. 線程和 Handler Looper MessageQueue 的關係

一個線程對應一個 Looper 對應一個 MessageQueue 對應多個 Handler。

6. 多個線程給 MessageQueue 發消息,如何保證線程安全

經過對 MessageQueue 加鎖來保證線程安全。

7. Handler 消息延遲是怎麼處理的

  1. 將傳入的延遲時間轉化成距離開機時間的毫秒數
  2. MessageQueue 中根據上一步轉化的時間進行順序排序
  3. 在 MessageQueue.next 獲取消息時,對比當前時間(now)和第一步轉化的時間(when),若是 now < when,則經過 epoll_wait 的 timeout 進行等待
  4. 若是該消息須要等待,會進行 idel handlers 的執行,執行完之後會再去檢查此消息是否能夠執行

8. View.post 和 Handler.post 的區別

View.post 最終也是經過 Handler.post 來執行消息的,執行過程以下:

  1. 若是在 performTraversals 前調用 View.post,則會將消息進行保存,以後在 dispatchAttachedToWindow 的時候經過 ViewRootImpl 中的 Handler 進行調用。
  2. 若是在 performTraversals 之後調用 View.post,則直接經過 ViewRootImpl 中的 Handler 進行調用。

9. Handler 致使的內存泄漏

略過不講~

10. 非 UI 線程真的不能操做 View 嗎

不能操做,緣由是 ViewRootImpl 會檢查建立 ViewRootImpl 的線程和當前操做的線程是否一致。而 ViewRootImpl 是在主線程建立的,因此非主線程不能操做 View。

最後

職業生涯歷來不是百米賽跑,而是馬拉松,不斷投資本身,得到能夠遷移的技能,獨立思考的能力,到中後期越是軟性的技能越能給你加成,願諸位工程師可以遠離焦慮,活出多彩的人生。

最近斷斷續續整理了一些面試題,目的是想了解一下大廠招聘的技術熱點,不斷提高學習,有興趣的朋友【點擊我】免費獲取哦。

篇幅有限,僅展現部份內容

相關文章
相關標籤/搜索