關於 Handler 的問題已經是一個老生常談的問題, 網上有很多優秀的文章講解 Handler, 之所以還要拿出來講這個問題, 是因為我發現, 在一些細節上面, 很多人還都似懂非懂, 面試的時候大家都能說出來一些東西, 但是又說不到點子上, 比如今天要說的這個問題: 為什麼Looper 中的 loop()方法不能導致主執行緒卡死??
先普及下 Android 訊息機制 的基礎知識:
Android 的訊息機制涉及了四個類:
- Handler: 訊息的傳送者和處理著
- Message: 訊息的載體
- MessageQueue: 訊息佇列
- Looper: 訊息迴圈體
其中每一條執行緒只有一個訊息佇列MessageQueue, 訊息的入隊是通過 MessageQueue 中的 enqueueMessage() 方法完成的, 訊息的出隊是通過Looper 中的loop()方法完成的.
Android 是單執行緒模型, UI的更新只能在主執行緒中執行, 在開發過程中, 不能在主執行緒中執行耗時的操作, 避免造成卡頓, 甚至導致ANR.
這裡面, 我故意把執行耗時這四個字突出, 我想大家在面試的時候說個這個問題, 但是造成介面卡頓甚至ANR的原因真的是執行耗時操作本省造成的嗎??
現在我們來寫個例子, 我們定義一個 button, 在 button 的 onClick 事件中寫一個死迴圈來模擬耗時操作, 程式碼很簡單, 例子如下:
@Override
public void onClick(View v) {
if (v.getId() == R.id.coordination) {
while (true) {
Log.i(TAG, "onClick: 耗時測試");
}
}
}複製程式碼
注意, 這裡我們執行程式, 然後點選按鈕以後, 接下來不做任何操作
執行程式以後, 你會發現, 我們的程式會已知列印 log, 並不會出現ANR的情況...
按照我們以往的想法, 如果我們在主執行緒中執行了耗時的操作, 這裡還是一個死迴圈, 那麼肯定會造成ANR的情況, 那為什麼我們的程式現在還在列印 log, 並沒有出現我們所想的ANR呢??
接下來讓我們繼續, 如果這時候你用手指去觸控螢幕, 比如再次點選按鈕或者點選我們的返回鍵, 你會發現5s 以後就出現了ANR....
其實前面的這個例子, 已經很好的說明了我們的問題. 之所以執行死迴圈不會導致ANR, 而在自迴圈以後觸控螢幕卻出發了ANR, 原因就是因為耗時操作本身並不會導致主執行緒卡死, 導致主執行緒卡死的真正原因是耗時操作之後的觸屏操作, 沒有在規定的時間內被分發。其實這也是我們標題索要討論的Looper 中的 loop()方法不會導致主執行緒卡死的原因之一。
看過 Looper 原始碼的都知道, 在 loop() 方法中也是有死迴圈的:
for (;;) {
//省略
}複製程式碼
前面我們說過, 死迴圈並不是導致主執行緒卡多的真正原因, 真正的原因是死迴圈後面的事件沒有得到分發, 那 loop()方法裡面也是一個死迴圈, 為什麼這個死迴圈後面的事件沒有出現問題呢??
熟悉Android 訊息機制的都知道, Looper 中的 loop()方法, 他的作用就是從訊息佇列MessageQueue 中不斷地取訊息, 然後將事件分發出去:
for (;;) {
/**
* 通過 MessageQueue.next() 方法不斷獲取訊息佇列中的訊息
*/
Message msg = queue.next(); // might block
if (msg == null) {//如果沒有訊息就會阻塞在這裡
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
/**
* 取出訊息以後呼叫 handler 的 dispatchMessage() 方法來處理訊息
*/
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}複製程式碼
最終呼叫的是 msg.target.dispatchMessage(msg) 將我們的事件分發出去, 所以不會造成卡頓或者ANR.
對於第一個原因, 我相信大家看那個對應的例子, 一定能看明白怎麼回事, 但是對於第二個原因,該如何去驗證呢??
想象一下, 我們自己寫的那個例子, 造成ANR是因為死迴圈後面的事件沒有在規定的事件內分發出去, 而 loop()中的死迴圈沒有造成ANR, 是因為 loop()中的作用就是用來分發事件的, 那麼如果我們讓自己寫的死迴圈擁有 loop()方法中同樣的功能, 也就是讓我們寫的死迴圈也擁有事件分發這個功能, 如果沒有造成死迴圈, 那豈不是就驗證了第二點原因?? 接下來我將我們的程式碼改造一下, 我們首先通過一個 Handler 將我們的死迴圈傳送到主執行緒的訊息佇列中, 然後將 loop() 方法中的部分程式碼 copy 過來, 讓我們的死迴圈擁有分發的功能:
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
Looper mainLooper = Looper.getMainLooper();
final Looper me = mainLooper;
final MessageQueue queue;
Field fieldQueue = me.getClass().getDeclaredField("mQueue");
fieldQueue.setAccessible(true);
queue = (MessageQueue) fieldQueue.get(me);
Method methodNext = queue.getClass().getDeclaredMethod("next");
methodNext.setAccessible(true);
Binder.clearCallingIdentity();
for (; ; ) {
Message msg = (Message) methodNext.invoke(queue);
if (msg == null) {
return;
}
msg.getTarget().dispatchMessage(msg);
msg.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});複製程式碼
執行程式碼後你會發現, 我們自己寫的死迴圈也不會造成ANR了!! 這也驗證了我們的第二個原因
到目前為止, 關於為什麼 Looper 中的 loop() 方法不會造成主執行緒阻塞的原因就分析完了, 主要有兩點原因:
- 耗時操作本身並不會導致主執行緒卡死, 導致主執行緒卡死的真正原因是耗時操作之後的觸屏操作, 沒有在規定的時間內被分發。
- Looper 中的 loop()方法, 他的作用就是從訊息佇列MessageQueue 中不斷地取訊息, 然後將事件分發出去。
後記:
關於這個問題, 我上 google 搜了一下, 發現網上有很多博主說原因是因為 linux 核心的 eoll 模型, native 層會通過讀寫檔案的方式來通知我們的主執行緒, 如果有事件就喚醒主執行緒, 如果沒有就讓主執行緒睡眠。
其實我個人的並不同意這個觀點, 這個有點所答非所謂, 如果說沒有事件讓主執行緒休眠是不會造成主執行緒卡死的原因, 那麼有事件的時候, 在忙碌的時候不也是在死迴圈嗎??那位什麼忙碌的時候沒有卡死呢?? 我個人認為 epoll 模型通過讀寫檔案通知主執行緒的作用, 應該是起到了節約資源的作用, 當沒有訊息就讓主執行緒休眠, 這樣可以節約 cpu 資源, 而並不是不會導致主執行緒卡死的原因。