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