為什麼Looper中的Loop()方法不能導致主執行緒卡死?
關於 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() 方法不會造成主執行緒阻塞的原因就分析完了, 主要有兩點原因:
1.耗時操作本身並不會導致主執行緒卡死, 導致主執行緒卡死的真正原因是耗時操作之後的觸屏操作, 沒有在規定的時間內被分發。
2.Looper 中的 loop()方法, 他的作用就是從訊息佇列MessageQueue 中不斷地取訊息, 然後將事件分發出去。
後記:
關於這個問題, 我上 google 搜了一下, 發現網上有很多博主說原因是因為 linux 核心的 eoll 模型, native 層會通過讀寫檔案的方式來通知我們的主執行緒, 如果有事件就喚醒主執行緒, 如果沒有就讓主執行緒睡眠。
其實我個人的並不同意這個觀點, 這個有點所答非所謂, 如果說沒有事件讓主執行緒休眠是不會造成主執行緒卡死的原因, 那麼有事件的時候, 在忙碌的時候不也是在死迴圈嗎??那位什麼忙碌的時候沒有卡死呢?? 我個人認為 epoll 模型通過讀寫檔案通知主執行緒的作用, 應該是起到了節約資源的作用, 當沒有訊息就讓主執行緒休眠, 這樣可以節約 cpu 資源, 而並不是不會導致主執行緒卡死的原因。
免費獲取安卓開發架構的資料(包括Fultter、高階UI、效能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線網際網路公司關於android面試的題目彙總可以加:936332305 / 連結:點選連結加入【安卓開發架構】: https://jq.qq.com/?_wv=1027&k=515xp64

在這裡插入圖片描述