android handle 阻塞,Android全面解析之Handler机制:常见问题汇总
主線程為什么不用初始化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在程序啟動的時候就跑起來了。我們不需要再去初始化主線程Looper。
為什么主線程的Looper是一個死循環,但是卻不會ANR?
答: 因為當Looper處理完所有消息的時候會進入阻塞狀態,當有新的Message進來的時候會打破阻塞繼續執行。
這其實沒理解好ANR這個概念。ANR,全名Application Not Responding。當我發送一個繪制UI 的消息到主線程Handler之后,經過一定的時間沒有被執行,則拋出ANR異常。Looper的死循環,是循環執行各種事務,包括UI繪制事務。Looper死循環說明線程沒有死亡,如果Looper停止循環,線程則結束退出了。Looper的死循環本身就是保證UI繪制任務可以被執行的原因之一。同時UI繪制任務有同步屏障,可以更加快速地保證繪制更快執行。同步屏障下面會講。
Handler如何保證MessageQueue并發訪問安全?
答:循環加鎖,配合阻塞喚醒機制。
我們可以發現MessageQueue其實是“生產者-消費者”模型,Handler不斷地放入消息,Looper不斷地取出,這就涉及到死鎖問題。如果Looper拿到鎖,但是隊列中沒有消息,就會一直等待,而Handler需要把消息放進去,鎖卻被Looper拿著無法入隊,這就造成了死鎖。Handler機制的解決方法是循環加鎖。在MessageQueue的next方法中:
Message next() {
...
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
...
}
}
}
我們可以看到他的等待是在鎖外的,當隊列中沒有消息的時候,他會先釋放鎖,再進行等待,直到被喚醒。這樣就不會造成死鎖問題了。
那在入隊的時候會不會因為隊列已經滿了然后一邊在等待消息處理一邊拿著鎖呢?這一點不同的是MessageQueue的消息沒有上限,或者說他的上限就是JVM給程序分配的內存,如果超出內存會拋出異常,但一般情況下是不會的。
Handler是如何切換線程的?
答: 使用不同線程的Looper處理消息。
前面我們聊到,代碼的執行線程,并不是代碼本身決定,而是執行這段代碼的邏輯是在哪個線程,或者說是哪個線程的邏輯調用的。每個Looper都運行在對應的線程,所以不同的Looper調用的dispatchMessage方法就運行在其所在的線程了。
Handler的阻塞喚醒機制是怎么回事?
答: Handler的阻塞喚醒機制是基于Linux的阻塞喚醒機制。
這個機制也是類似于handler機制的模式。在本地創建一個文件描述符,然后需要等待的一方則監聽這個文件描述符,喚醒的一方只需要修改這個文件,那么等待的一方就會收到文件從而打破喚醒。
能不能讓一個Message加急被處理?/ 什么是Handler同步屏障?
答:可以 / 一種使得異步消息可以被更快處理的機制
如果向主線程發送了一個UI更新的操作Message,而此時消息隊列中的消息非常多,那么這個Message的處理就會變得緩慢,造成界面卡頓。所以通過同步屏障,可以使得UI繪制的Message更快被執行。
什么是同步屏障?這個“屏障”其實是一個Message,插入在MessageQueue的鏈表頭,且其target==null。Message入隊的時候不是判斷了target不能為null嗎?不不不,添加同步屏障是另一個方法:
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有什么用呢?看next方法:
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;
}
...
}
...
}
}
這個方法我在前面講過,我們重點看一下關于同步屏障的部分,看注釋1的地方的代碼:
if (msg != null && msg.target == null) {
// 同步屏障,找到下一個異步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
如果遇到同步屏障,那么會循環遍歷整個鏈表找到標記為異步消息的Message,即isAsynchronous返回true,其他的消息會直接忽視,那么這樣異步消息,就會提前被執行了。注釋2的代碼注意一下就可以了。
注意,同步屏障不會自動移除,使用完成之后需要手動進行移除,不然會造成同步消息無法被處理。從源碼中可以看到如果不移除同步屏障,那么他會一直在那里,這樣同步消息就永遠無法被執行了。
有了同步屏障,那么喚醒的判斷條件就必須再加一個:MessageQueue中有同步屏障且處于阻塞中,此時插入在所有異步消息前插入新的異步消息。這個也很好理解,跟同步消息是一樣的。如果把所有的同步消息先忽視,就是插入新的鏈表頭且隊列處于阻塞狀態,這個時候就需要被喚醒了。看一下源碼:
boolean enqueueMessage(Message msg, long when) {
...
// 對MessageQueue進行加鎖
synchronized (this) {
...
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
/**
* 1
*/
// 當線程被阻塞,且目前有同步屏障,且入隊的消息是異步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
/**
* 2
*/
// 如果找到一個異步消息,說明前面有延遲的異步消息需要被處理,不需要被喚醒
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
// 如果需要則喚醒隊列
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
同樣,這個方法我之前講過,把無關同步屏障的代碼忽視,看到注釋1處的代碼。如果插入的消息是異步消息,且有同步屏障,同時MessageQueue正處于阻塞狀態,那么就需要喚醒。而如果這個異步消息的插入位置不是所有異步消息之前,那么不需要喚醒,如注釋2。
那我們如何發送一個異步類型的消息呢?有兩種辦法:
使用異步類型的Handler發送的全部Message都是異步的
給Message標志異步
Handler有一系列帶Boolean類型的參數的構造器,這個參數就是決定是否是異步Handler:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
// 這里賦值
mAsynchronous = async;
}
在發送消息的時候就會給Message賦值:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
// 賦值
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
但是異步類型的Handler構造器是標記為hide,我們無法使用,所以我們使用異步消息只有通過給Message設置異步標志:
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
但是!!!!,其實同步屏障對于我們的日常使用的話其實是沒有多大用處。因為設置同步屏障和創建異步Handler的方法都是標志為hide,說明谷歌不想要我們去使用他。所以這里同步屏障也作為一個了解,可以更加全面地理解源碼中的內容。
什么是IdleHandler?
答: 當MessageQueue為空或者目前沒有需要執行的Message時會回調的接口對象。
IdleHandler看起來好像是個Handler,但他其實只是一個有單方法的接口,也稱為函數型接口:
public static interface IdleHandler {
boolean queueIdle();
}
在MessageQueue中有一個List存儲了IdleHandler對象,當MessageQueue沒有需要被執行的Message時就會遍歷回調所有的IdleHandler。所以IdleHandler主要用于在消息隊列空閑的時候處理一些輕量級的工作。
IdleHandler的調用是在next方法中:
Message next() {
// 如果looper已經退出了,這里就返回null
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
// IdleHandler的數量
int pendingIdleHandlerCount = -1;
// 阻塞時間
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 阻塞對應時間
nativePollOnce(ptr, nextPollTimeoutMillis);
// 對MessageQueue進行加鎖,保證線程安全
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
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;
// 一般只有異步消息才會從中間拿走消息,同步消息都是從鏈表頭獲取
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;
}
// 當隊列中的消息用完了或者都在等待時間延遲執行同時給pendingIdleHandlerCount<0
// 給pendingIdleHandlerCount賦值MessageQueue中IdleHandler的數量
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
// 沒有需要執行的IdleHanlder直接continue
if (pendingIdleHandlerCount <= 0) {
// 執行IdleHandler,標記MessageQueue進入阻塞狀態
mBlocked = true;
continue;
}
// 把List轉化成數組類型
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 執行IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // 釋放IdleHandler的引用
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
// 如果返回false,則把IdleHanlder移除
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// 最后設置pendingIdleHandlerCount為0,防止再執行一次
pendingIdleHandlerCount = 0;
// 當在執行IdleHandler的時候,可能有新的消息已經進來了
// 所以這個時候不能阻塞,要回去循環一次看一下
nextPollTimeoutMillis = 0;
}
}
代碼很多,可能看著有點亂,我梳理一下邏輯,然后再回去看源碼就會很清晰了:
當調用next方法的時候,會給pendingIdleHandlerCount賦值為-1
如果隊列中沒有需要處理的消息的時候,就會判斷pendingIdleHandlerCount是否為<0,如果是則把存儲IdleHandler的list的長度賦值給pendingIdleHandlerCount
把list中的所有IdleHandler放到數組中。這一步是為了不讓在執行IdleHandler的時候List被插入新的IdleHandler,造成邏輯混亂
然后遍歷整個數組執行所有的IdleHandler
最后給pendingIdleHandlerCount賦值為0。然后再回去看一下這個期間有沒有新的消息插入。因為pendingIdleHandlerCount的值為0不是-1,所以IdleHandler只會在空閑的時候執行一次
同時注意,如果IdleHandler返回了false,那么執行一次之后就被丟棄了。
建議讀者再回去把源碼看一遍,這樣邏輯會清晰很多。
最后
到這里,Handler系列的文章就結束了。如果有地方遺漏沒介紹到,或者有疑問,可評論區留言。 感謝閱讀。
大家如果還想了解更多Android 相關的更多知識點可以點進我的【GitHub】項目中,里面記錄了許多的Android 知識點。
總結
以上是生活随笔為你收集整理的android handle 阻塞,Android全面解析之Handler机制:常见问题汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: css --- flex:n的解析
- 下一篇: 视频教程| Egret 打包Androi