面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

南方吳彥祖_藍斯發表於2020-10-12

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

一、題目層次

  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 嗎

二、題目詳解

程式碼分析基於 Android SDK 28

大家可以先看上面的問題思考一下,如果都清楚的話,下面的文章也沒必要看了~

1. Handler 的基本原理

關於 Handler 的原理,相比不用多說了,大家都應該知道,一張圖就可以說明(圖片來自網路)。

面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

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 裡做了什麼事情呢?

    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 中的訊息,進行執行了。

這裡一般會引申一個問題,就是主執行緒中為什麼不用手動呼叫這兩個方法呢?相信大家也都明白,就是 ActivityThread.main 中已經進行了呼叫。

透過這個問題,又可以引申到 ActivityThread 相關的知識,這裡就不細說了。

3. MessageQueue 如何等待訊息

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

透過 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.cppstatic 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.cppint 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 機制這裡就不進行過多介紹了,大家有興趣可以參考  segmentfault.com/a/119000000…

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

4. 為什麼不用 wait 而用 epoll 呢?

說起來 java 中的 wait / notify 也能實現阻塞等待訊息的功能,在 Android 2.2 及以前,也確實是這樣做的。
可以參考這個 commit  /android/2.1…

那為什麼後面要改成使用 epoll 呢?透過看 commit 記錄,是需要處理 native 側的事件,所以只使用 java 的 wait / notify 就不夠用了。
具體的改動就是這個 commit  android.googlesource.com/platform/fr…

Sketch of Native input for MessageQueue / Looper / ViewRootMessageQueue now uses a socket for internal signalling, and is prepared
to also handle any number of event input pipes, once the plumbing isset up with ViewRoot / Looper to tell it about them as appropriate.Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3

不過這裡最開始使用的還是 select,後面才改成 epoll。
具體可見這個 commit  android.googlesource.com/platform/fr…

至於 select 和 epoll 的區別,這裡也不細說了,大家可以在上面的參考文章中一起看看。

5. 執行緒和 Handler Looper MessageQueue 的關係

這裡的關係是一個執行緒對應一個 Looper 對應一個 MessageQueue 對應多個 Handler。

6. 多個執行緒給 MessageQueue 發訊息,如何保證執行緒安全

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

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

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

Handler 引申的另一個問題就是延遲訊息在 Handler 中是怎麼處理的?定時器還是其他方法?
這裡我們先從事件發起開始看起:

// Handler.javapublic 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.javapublic 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.javafinal 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。

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

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

// ViewGroupvoid 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()));
    }}// Viewvoid dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...}

看到這裡,大家可能忘記我們開始剛剛要做什麼了。

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

// View.javapublic 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.javapublic 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 過不了這裡的檢查。

三、總結

一個小小的 Handler,其實可以引申出很多問題,這裡這是列舉了一些大家可能忽略的問題,更多的問題就等待大家去探索了~
這裡來總結一下:

1. Handler 的基本原理

一張圖解釋(圖片來自網路)

面試官帶你學Android——面試中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。

今天的文章就結束了,希望大家能學到一些不一樣的知識~

面試前該如何複習?

其實客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。

這裡再分享一下我面試期間的複習路線:(以下體系的複習資料是我從各路大佬收集整理好的)

《Android開發七大模組核心知識筆記》

面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?
面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?
面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

《960全網最全Android開發筆記》

面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

《379頁Android開發面試寶典》

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、360、新浪、搜狐等一線網際網路公司面試被問到的題目。熟悉本文中列出的知識點會大大增加透過前兩輪技術面試的機率。

如何使用它?

1.可以透過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

《507頁Android開發相關原始碼解析》

只要是程式設計師,不管是Java還是Android,如果不去閱讀原始碼,只看API文件,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀原始碼,不僅限於閱讀各大系統原始碼,還包括各種優秀的開源庫。

面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?

資料太多,全部展示會影響篇幅,暫時就先列舉這些部分截圖,以上資源均免費分享,以上內容均放在了開源專案: github  中已收錄,大家可以自行獲取。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2726413/,如需轉載,請註明出處,否則將追究法律責任。

相關文章