關於 Handler 的問題已是一個老生常談的問題, 網上有不少優秀的文章講解 Handler, 之因此還要拿出來說這個問題, 是由於我發現, 在一些細節上面, 不少人還都似懂非懂, 面試的時候你們都能說出來一些東西, 可是又說不到點子上, 好比今天要說的這個問題: 爲何Looper 中的 loop()方法不能致使主線程卡死??linux
先普及下 Android 消息機制 的基礎知識: 面試
Android 的消息機制涉及了四個類:bash
其中每一條線程只有一個消息隊列MessageQueue, 消息的入隊是經過 MessageQueue 中的 enqueueMessage() 方法完成的, 消息的出隊是經過Looper 中的loop()方法完成的.ide
Android 是單線程模型, UI的更新只能在主線程中執行, 在開發過程當中, 不能在主線程中執行耗時的操做, 避免形成卡頓, 甚至致使ANR. oop
這裏面, 我故意把執行耗時這四個字突出, 我想你們在面試的時候說個這個問題, 可是形成界面卡頓甚至ANR的緣由真的是執行耗時操做本省形成的嗎??post
如今咱們來寫個例子, 咱們定義一個 button, 在 button 的 onClick 事件中寫一個死循環來模擬耗時操做, 代碼很簡單, 例子以下: 測試
@Override
public void onClick(View v) {
if (v.getId() == R.id.coordination) {
while (true) {
Log.i(TAG, "onClick: 耗時測試");
}
}
}複製代碼
注意, 這裏咱們運行程序, 而後點擊按鈕之後, 接下來不作任何操做ui
運行程序之後, 你會發現, 咱們的程序會已知打印 log, 並不會出現ANR的狀況...google
按照咱們以往的想法, 若是咱們在主線程中執行了耗時的操做, 這裏仍是一個死循環, 那麼確定會形成ANR的狀況, 那爲何咱們的程序如今還在打印 log, 並無出現咱們所想的ANR呢??spa
接下來讓咱們繼續, 若是這時候你用手指去觸摸屏幕, 好比再次點擊按鈕或者點擊咱們的返回鍵, 你會發現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() 方法不會形成主線程阻塞的緣由就分析完了, 主要有兩點緣由:
關於這個問題, 我上 google 搜了一下, 發現網上有不少博主說緣由是由於 linux 內核的 eoll 模型, native 層會經過讀寫文件的方式來通知咱們的主線程, 若是有事件就喚醒主線程, 若是沒有就讓主線程睡眠。
其實我我的的並不一樣意這個觀點, 這個有點所答非所謂, 若是說沒有事件讓主線程休眠是不會形成主線程卡死的緣由, 那麼有事件的時候, 在忙碌的時候不也是在死循環嗎??那位什麼忙碌的時候沒有卡死呢?? 我我的認爲 epoll 模型經過讀寫文件通知主線程的做用, 應該是起到了節約資源的做用, 當沒有消息就讓主線程休眠, 這樣能夠節約 cpu 資源, 而並非不會致使主線程卡死的緣由。