Android Handler面試總結

xiangzhihong發表於2022-03-18

在Android面試中,有關Handler的面試是一個離不開的話題,下面我們就有關Handler的面試進行一個總結。

1,Handler、Looper、MessageQueue、執行緒的關係

  • 一個執行緒只會有一個Looper物件,所以執行緒和Looper是一一對應的。
  • MessageQueue物件是在new Looper的時候建立的,所以Looper和MessageQueue是一一對應的。
  • Handler的作用只是將訊息加到MessageQueue中,並後續取出訊息後,根據訊息的target欄位分發給當初的那個handler,所以Handler對於Looper是可以多對一的,也就是多個Hanlder物件都可以用同一個執行緒、同一個Looper、同一個MessageQueue。

綜上,Looper、MessageQueue、執行緒是一一對應關係,而他們與Handler是可以一對多的。

2,主執行緒為什麼不用初始化Looper

因為應用在啟動的過程中就已經初始化了一個主執行緒Looper。每個java應用程式都是有一個main方法入口,Android是基於Java的程式也不例外,Android程式的入口在ActivityThread的main方法中,程式碼如下:

// 初始化主執行緒Looper
 Looper.prepareMainLooper();
 ...
 // 新建一個ActivityThread物件
 ActivityThread thread = new ActivityThread();
 thread.attach(false, startSeq);
 // 獲取ActivityThread的Handler,也是他的內部類H
 if (sMainThreadHandler == null) {
 sMainThreadHandler = thread.getHandler();
 }
 ...
 Looper.loop();
 // 如果loop方法結束則丟擲異常,程式結束
 throw new RuntimeException("Main thread loop unexpectedly exited");
} 

可以看到,main方法中會先初始化主執行緒Looper,新建ActivityThread物件,然後再啟動Looper,這樣主執行緒的Looper在程式啟動的時候就跑起來了。並且,我們通常認為 ActivityThread 就是主執行緒,事實上它並不是一個執行緒,而是主執行緒操作的管理者。

3,為什麼主執行緒的Looper是一個死迴圈,但是卻不會ANR

因為當Looper處理完所有訊息的時候會進入阻塞狀態,當有新的Message進來的時候會打破阻塞繼續執行。

首先,我們看一下什麼是ANR,ANR,全名Application Not Responding。當我傳送一個繪製UI 的訊息到主執行緒Handler之後,經過一定的時間沒有被執行,則丟擲ANR異常。下面再來回答一下,主執行緒的Looper為什麼是一個死迴圈,卻不會ANR?Looper的死迴圈,是迴圈執行各種事務,包括UI繪製事務。Looper死迴圈說明執行緒沒有死亡,如果Looper停止迴圈,執行緒則結束退出了,Looper的死迴圈本身就是保證UI繪製任務可以被執行的原因之一。

關於這個問題,我們還可以得到如下的一些結論:

  • 真正會卡死的操作是在某個訊息處理的時候操作時間過長,導致掉幀、ANR,而不是loop方法本身。
  • 在主執行緒以外,會有其他的執行緒來處理接受其他程式的事件,比如Binder執行緒(ApplicationThread),會接受AMS傳送來的事件
  • 在收到跨程式訊息後,會交給主執行緒的Hanlder再進行訊息分發。所以Activity的生命週期都是依靠主執行緒的Looper.loop,當收到不同Message時則採用相應措施,比如收到msg=H.LAUNCH_ACTIVITY,則呼叫ActivityThread.handleLaunchActivity()方法,最終執行到onCreate方法。
  • 當沒有訊息的時候,會阻塞在loop的queue.next()中的nativePollOnce()方法裡,此時主執行緒會釋放CPU資源進入休眠狀態,直到下個訊息到達或者有事務發生,所以死迴圈也不會特別消耗CPU資源。

4,Message是怎麼找到它所屬的Handler然後進行分發的

在loop方法中,找到要處理的Message需要呼叫下面的一段程式碼來處理訊息:

msg.target.dispatchMessage(msg);

所以是將訊息交給了msg.target來處理,那麼這個target是什麼呢,通常檢視target的源頭可以發現:

private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {
        msg.target = this;

        return queue.enqueueMessage(msg, uptimeMillis);
    }

在使用Hanlder傳送訊息的時候,會設定msg.target = this,所以target就是當初把訊息加到訊息佇列的那個Handler。

5,Handler是如何切換執行緒的

使用不同執行緒的Looper處理訊息。我們知道,程式碼的執行執行緒,並不是程式碼本身決定,而是執行這段程式碼的邏輯是在哪個執行緒,或者說是哪個執行緒的邏輯呼叫的。每個Looper都執行在對應的執行緒,所以不同的Looper呼叫的dispatchMessage方法就執行在其所在的執行緒了。

6,post(Runnable) 與 sendMessage 有什麼區別

我們知道,Hanlder中傳送訊息可以分為兩種:post(Runnable)和sendMessage。首先,我們來看一下原始碼:

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

 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
   
public final boolean sendMessage(@NonNull Message msg) {
     return sendMessageDelayed(msg, 0);
   }

可以看到,post和sendMessage的區別就在於,post方法給Message設定了一個callback回撥。那麼,那麼這個callback有什麼用呢?我們再轉到訊息處理的方法dispatchMessage中看:

public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

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

可以看到,如果msg.callback不為空,也就是通過post方法傳送訊息的時候,會把訊息交給這個msg.callback進行處理;如果msg.callback為空,也就是通過sendMessage傳送訊息的時候,會判斷Handler當前的mCallback是否為空,如果不為空就交給Handler.Callback.handleMessage處理。

所以post(Runnable) 與 sendMessage的區別就在於後續訊息的處理方式,是交給msg.callback還是 Handler.Callback或者Handler.handleMessage。

7,Handler如何保證MessageQueue併發訪問安全的

迴圈加鎖,配合阻塞喚醒機制。我們發現,MessageQueue其實是【生產者-消費者】模型,Handler不斷地放入訊息,Looper不斷地取出,這就涉及到死鎖問題。如果Looper拿到鎖,但是佇列中沒有訊息,就會一直等待,而Handler需要把訊息放進去,鎖卻被Looper拿著無法入隊,這就造成了死鎖,Handler機制的解決方法是迴圈加鎖,程式碼在MessageQueue的next方法中:

Message next() {
 ...
 for (;;) {
 ...
 nativePollOnce(ptr, nextPollTimeoutMillis);
 synchronized (this) {
 ...
 }
 }
} 

我們可以看到他的等待是在鎖外的,當佇列中沒有訊息的時候,他會先釋放鎖,再進行等待,直到被喚醒。這樣就不會造成死鎖問題了。

8,Handler的阻塞喚醒機制是怎麼實現的

Handler的阻塞喚醒機制是基於Linux的阻塞喚醒機制。這個機制也是類似於handler機制的模式。在本地建立一個檔案描述符,然後需要等待的一方則監聽這個檔案描述符,喚醒的一方只需要修改這個檔案,那麼等待的一方就會收到檔案從而打破喚醒。

參考:Linux的阻塞喚醒機制

9,什麼是Handler的同步屏障

所謂同步屏障,其實就是一個Message,只不過它是插入在MessageQueue的連結串列頭,且其target==null。 而Message加急訊息就是使用同步屏障實現的。同步屏障用到了postSyncBarrier()方法。

public int postSyncBarrier() {
 return postSyncBarrier(SystemClock.uptimeMillis());
}
 private int postSyncBarrier(long when) {
 synchronized (this) {
 final int token = mNextBarrierToken++;
 final Message msg = Message.obtain();
 msg.markInUse();
 msg.when = when;
 msg.arg1 = token;
 Message prev = null;
 Message p = mMessages;
 // 把當前需要執行的Message全部執行
 if (when != 0) {
 while (p != null && p.when <= when) {
 prev = p;
 p = p.next;
 }
 }
 // 插入同步屏障
 if (prev != null) { // invariant: p == prev.next
 msg.next = p;
 prev.next = msg;
 } else {
 msg.next = p;
 mMessages = msg;
 }
 return token;
 }
} 

可以看到,同步屏障就是一個特殊的target,即target==null,我們可以看到他並沒有給target屬性賦值,那這個target有什麼用呢?

Message next() {
 ...
 // 阻塞時間
 int nextPollTimeoutMillis = 0;
 for (;;) {
 ...
 // 阻塞對應時間 
 nativePollOnce(ptr, nextPollTimeoutMillis);
 // 對MessageQueue進行加鎖,保證執行緒安全
 synchronized (this) {
 final long now = SystemClock.uptimeMillis();
 Message prevMsg = null;
 Message msg = mMessages;
 /**
 *  1
 */
 if (msg != null && msg.target == null) {
 // 同步屏障,找到下一個非同步訊息
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
 }
 if (msg != null) {
 if (now < msg.when) {
 // 下一個訊息還沒開始,等待兩者的時間差
 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
 } else {
 // 獲得訊息且現在要執行,標記MessageQueue為非阻塞
 mBlocked = false;
 /**
 *  2
 */
 // 一般只有非同步訊息才會從中間拿走訊息,同步訊息都是從連結串列頭獲取
 if (prevMsg != null) {
 prevMsg.next = msg.next;
 } else {
 mMessages = msg.next;
 }
 msg.next = null;
 msg.markInUse();
 return msg;
 }
 } else {
 // 沒有訊息,進入阻塞狀態
 nextPollTimeoutMillis = -1;
 }
 // 當呼叫Looper.quitSafely()時候執行完所有的訊息後就會退出
 if (mQuitting) {
 dispose();
 return null;
 }
 ...
 }
 ...
 }
} 

我們重點看一下關於同步屏障的部分程式碼。

if (msg != null && msg.target == null) {
 // 同步屏障,找到下一個非同步訊息
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
} 

如果遇到同步屏障,那麼會迴圈遍歷整個連結串列找到標記為非同步訊息的Message,即isAsynchronous返回true,其他的訊息會直接忽視,那麼這樣非同步訊息,就會提前被執行了。同時,,同步屏障不會自動移除,使用完成之後需要手動進行移除,不然會造成同步訊息無法被處理。

10,IdleHandler的使用場景

前面說過,當MessageQueue沒有訊息的時候,就會阻塞在next方法中,其實在阻塞之前,MessageQueue還會做一件事,就是檢查是否存在IdleHandler,如果有,就會去執行它的queueIdle方法。

IdleHandler看起來好像是個Handler,但他其實只是一個有單方法的介面,也稱為函式型介面。

public static interface IdleHandler {
 boolean queueIdle();
} 

事實上,在MessageQueue中有一個List儲存了IdleHandler物件,當MessageQueue沒有需要被執行的Message時就會遍歷回撥所有的IdleHandler。所以IdleHandler主要用於在訊息佇列空閒的時候處理一些輕量級的工作。

因此,IdleHandler可以用來進行啟動優化,比如將一些事件(比如介面view的繪製、賦值)放到onCreate方法或者onResume方法中。但是這兩個方法其實都是在介面繪製之前呼叫的,也就是說一定程度上這兩個方法的耗時會影響到啟動時間,所以我們可以把一些操作放到IdleHandler中,也就是介面繪製完成之後才去呼叫,這樣就能減少啟動時間了。

11,HandlerThread使用場景

首先,我們來看一下HandlerThread的原始碼:

public class HandlerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }

可以看到,HandlerThread是一個封裝了Looper的Thread類,就是為了讓我們在子執行緒裡面更方便的使用Handler。這裡的加鎖就是為了保證執行緒安全,獲取當前執行緒的Looper物件,獲取成功之後再通過notifyAll方法喚醒其他執行緒,那哪裡呼叫了wait方法呢?答案是getLooper方法。

public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }

        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。

相關文章