通過前面的知識Android Handler消息機制我們知道了Handler的工作原理,同時也產生了一些問題,比如Looper.loop()開啟了死循環,不會阻塞線程么?不會被卡死么?
導讀
- 移動開發知識體系總章(Java基礎、Android、Flutter)
- Android中為什么主線程不會因為Looper.loop里的無限循環ANR?
- Looper.loop和MessageQueue.next分析
- ANR
Android中為什么主線程不會因為Looper.loop里的無限循環ANR?
先看看進程和線程:
進程:每個app運行前會先創建一個進程,該進程是由Zygote fork出來的,用于承載App上運行的各種Activity/Service等組件。進程對于上層應用來說是完全透明的,App程序都是運行在Android Runtime。大多數情況一個App就運行在一個進程中(在AndroidManifest.xml中配置Android:process屬性,或通過native代碼fork進程)。并且至少有一個線程。
線程:依附于進程,是分配和調度CPU的最小單元,由于依附關系,線程之間是可以資源共享的,但進程不行,從Linux角度來說進程與線程除了是否共享資源外,并沒有本質的區別,都是一個task_struct結構體,在CPU看來進程、線程無非就是一段可執行的代碼,CPU采用CFS調度算法,保證每個task都盡可能公平的享有CPU時間片。
對于進程和線程的區別,我看到過這樣一個比喻:把系統比作一個公司,公司會分配一個辦公場所(擁有獨立的用戶空間)給某個部門的部門(部門或部門經理相當于進程了),會有核心項目需要執行(可執行程序代碼),部門至少有一個或多個員工(即線程),而員工歸屬于部門(或部門經理)(線程依附于進程),在這個部門下員工之間非常好溝通(零食啥的隨便分享),但部門與部門間有啥事就得通過溝通了(不能直接共享,需要通信)
對于主線程,若應用運行一下就自己就結束了,肯定不行的,那么如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,無限循環便能保證不會被退出,而ActivityThread的main方法主要就是做消息循環,主角就是Looper。
再看看程序無響應(ANR)和線程阻塞、結束
- 程序無響應:主線程執行事件的時間過長,導致其他需要立刻在主線程處理的事件無法得到處理。
- 線程阻塞:線程處于等待狀態。
- 線程結束:一般線程執行完run方法之后,線程就正常結束了。
1.阻塞和程序無響應沒有必然的關系,雖然主線程在沒有消息可處理的時候是阻塞的,但是只要保證有消息的時候能夠立即處理,程序是不會無響應的。
2.阻塞和線程退出也沒有必然的關系,線程完全可以在不阻塞的情況下死循環,同樣達到不退出的效果。阻塞考慮到節約系統而做的處理,和線程退出沒有關系。
3.ActivityThread的main方法主要就是做消息循環,一旦退出消息循環,那么應用就退出了,Looper.loop方法可能會引起主線程阻塞,但只要消息循環沒有被阻塞,就一直能處理事件就不會產生ANR異常。
4.消息隊列是一個無限循環,為什么無限循環不會ANR?因為應用的整個生命周期就是運行在這個消息循環中的,Android是由事件驅動的,Looper.loop方法不斷的接受處理事件,每個點擊觸摸或者Activity每個生命周期都是在Looper.loop方法的控制之下,Looper.loop方法一旦結束,應用程序的生命周期也就結束了。
5.什么情況下會發生ANR?
第一,事件沒有得到處理;
第二,事件正在處理,但是沒有及時完成。
結論:
所以只能說事件的處理如果阻塞導致ANR,而不能說是Looper.loop的無限循環會ANR。
這就是說為什么在Activity生命周期onCreate/onStart/onResume等方法進行耗時的操作會ANR了。
這也是以下情況容易ANR的根本原因
Activity響應時間超過5s無響應
Broadcast在處理時間超過10s無響應
Service處理時間超過20s無響應
筆者新建了一個demo,創建了頁面一(有一個跳轉頁面二的按鈕)、頁面二(在子線程中開啟了一個無限循環),運行后,頁面二果然沒有ANR。(代碼很簡單就不貼了)。
頁面一 | 頁面二(子線程無限循環的) |
---|---|
頁面一、二CPU\內存對比結果:
1、頁面一無CPU消耗,頁面二CPU平均消耗25%。
2、頁面一內存消耗約10M,頁面二內存消耗約12M。
3、在頁面一直接新增無限循環,直接ANR。
4、在頁面二無限循環新增Thread.sleep(200000);進行休眠,發現程序并沒有ANR,而且CPU沒有消耗,內存多消耗約1M。
(說明:只有主線程中執行任務事件過長,導致其他需要立刻在主線程處理的事件無法得到處理,才有可能會ANR).
5、在頁面二無限循環新增簡單處理邏輯,CPU和內存消耗會小幅波動。
6、在頁面二無限循環新增了讀取drawable為Bitmap并用list一直add,此時的CPU波動非常平穩保持22%,而內存消耗則逐步提升,很快又會被降低,在約23分鐘后后由于OutOfMemory而停止運行。
Looper.loop和MessageQueue.next分析
先查看loop()的源碼:
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
}
}
在for循環里,queue.next(); 寫著注釋might block,說可能會阻塞!查看next關鍵代碼:
Message next() {
...
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
...
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
...
return msg;
}
}
}
}
}
1、MessageQueue.next內部也是一個for (;;)無限循環。
2、正常情況下,有消息,直接return msg,繼續執行loop()中的方法。
2、msg為空或者msg指向的Handler為空(target)時,,則會執行dowhile,去取隊列中的下一個消息,值得取出來為止,反之會無限循環著!
3、Looper中有無限循環,MessageQueue中也有無限循環,我們都知道新建一個項目跑起來是沒有任何問題的,也就是說,Looper、MessageQueue中的無限循環不會ANR,
(還不知道msg.target為什么是Handler?Hanlder源碼中明確寫著Message.target == this)