知識梳理系列之二——消息機制中的若干重要問題
這是一個老生常談的知識,本文不是全面文章,主要記錄一些非常有用的原理性知識點,方便深入理解。
輪詢器Looper
子線程爲何要手動準備和輪詢?
一、MainThread的Looper創建和準備
Android 的啓動過程是: 創建init進程 --> Zygote進程 --> SystemServer進程 --> 各個應用進程
在SystemServer進程啓動後(由Zygote進程fork出)在調用run方法時,調用了Looper.prepareMainLooper();,在老版本的則是在ActivityThread.main()中調用的prepareMainLooper
// SystemServer.java
package com.android.server;
public final class SystemServer {
...
public static void main(String[] args) {
new SystemServer().run();
}
private void run() {
...
Looper.prepareMainLooper();
...
startBootstrapService();
startCoreService();
startOtherService();
...
Looper.loop();
...
}
...
}
於是,主線程在啓動後,Activity#onCreate(Bundle) 調用時候就已經準備好Looper了
而工作線程Looper是沒有調用prepare()/loop()的,因此需要自己手動調用或者使用HandlerThread/IntentService
如何保證線程Looper唯一?
遇到一個問Looper存儲在哪裏的問題
我們來看下Looper.java
// Looper.java
...
public static void prepare() {
// 工作線程的prepare傳入了true,表示允許消息隊列退出
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
// 在調用prepare的時候如果ThreadLocal中就已經存在Looper了
// 則拋出異常,避免重複prepare和同一個線程出現多個Looper對象
throw new RuntimeException("Only one Looper may be created per thread");
}
// 向ThreadLocal 中存入創建的Looper對象
sThreadLocal.set(new Looper(quitAllowed));
}
public static void prepareMainLooper() {
// 主線程的prepare傳入了false,表示主線程的消息隊列是不允許對出的!!
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
// 此處爲Looper對象的成員變量sMainLooper初始化
// myLooper()方法實際還是調用ThreadLocal.get()
sMainLooper = myLooper();
}
}
所以我們有答案了,Looper是保存在ThreadLocal這個線程本地存儲裏的,每一個線程只有一個線程本地存儲,所以確保了同一個線程只有一個Looper!
附:ThreadLocal是怎樣的數據結構:
// Looper.java
// Looper類中有靜態常量ThreadLocal對象,通過ThreadLocal.get()獲取Looper實例(線程唯一)
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
// ThreadLocal.java
// get方法獲取了當前線程,通過線程取得線程的ThreadLocalMap引用,然後根據指定泛型對象獲取實例
// 簡單說就是從Map中獲取了ThreadLocal<Looper>的鍵值對對象Entry,最後返回Entry.value即Looper對象
// 而這個Map是一個哈希數組,key是ThreadLocal<?>對象,就是說只有唯一的一個ThreadLocal<Looper>,
// 因爲ThreadLocal<Looper>與別的泛型的ThreadLocal對象的hash值不同,因此確保了一個線程只有一個Looper
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
總結:
1. 主線程Looper在啓動時就已經prepare/loop,子線程需要手動執行或使用HandlerThread/IntentService;
2. Looper被存儲在相應Thread的ThreadLocal對象中,這個對象是一個哈希數組,保證了Looper線程唯一;
Looper.loop中是死循環爲什麼MainLooper不會阻塞主線程
在loop方法中,實際是一個死循環,即不斷的從消息隊列中獲取消息,並分發給target(Handler)來處;
那麼主線程是如何不會被這個死循環卡死的?
簡化loop方法如下:
//Looper.java
public static void loop() {
// 獲取不到Looper 拋出異常
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException(
"No Looper; Looper.prepare() wasn't called on this thread.");
}
// 獲取Looper 持有的消息隊列
final MessageQueue queue = me.mQueue;
...
for (;;) {
// 進入死循環 不斷從消息隊列中獲取下一個消息
// 官方在這裏註釋了might block表示獲取下一個消息有可能會阻塞
Message msg = queue.next(); // might block
if (msg == null) {// 拿到空消息退出循環 這時候loop也退出了
// No message indicates that the message queue is quitting.
return;
}
...
try {
// 把非空消息分發給目標Handler
msg.target.dispatchMessage(msg);
}
...
// 對消息進行回收
msg.recycleUnchecked();
}
}
通過loop方法可以看出端倪,死循環在 queue.next() 方法處阻塞
再看簡化的MessageQueue.next方法
// MessageQueue.java
Message next() {
// MessageQueue持有Native消息隊列的指針,如果已經退出了就會返回空消息
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
// 再進入一個死循環,調用本地方法去檢測描述符有沒有新的消息可以poll出來
// 參數nextPollTimeOutMillis表示的是沒有檢測到新消息時的超時阻塞時間,-1表示一直阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
// 休眠被喚醒後執行同步代碼塊
synchronized (this) {
// 獲取系統時間 和 消息隊列頭部Message
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 省略同步屏障有關處理
...
if (msg != null) {
if (now < msg.when) {
// 如果消息的時間沒有到(比如延時消息),重新計算nextPollTimeoutMillis
// 下一次循環,nativePollOnce阻塞的時間就是確定的
nextPollTimeoutMillis = (int) Math.min(
msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
// 有同步屏障纔會走這裏
prevMsg.next = msg.next;
} else {
// 更新消息隊列對頭
mMessages = msg.next;
}
// 修改use標記和從消息隊列中剝離出msg,最後返回msg
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// 取到空消息,重置nextPollTimeoutMillis 繼續循環
nextPollTimeoutMillis = -1;
}
...
}
...
}
至此,已經找到問題的關鍵部分了。
Looper.loop()方法的死循環是在MessageQueue.next()獲取隊列中下一個消息時,阻塞的,並且如果主線程消息隊列一直沒有新消息時,就會一直阻塞,是被一個nativePollOnce()的本地方法阻塞的。
回到問題,阻塞了爲什麼沒有導致主線程卡死。由於 Linux pipe/epoll機制,主線程會在此時(沒有新的消息和事件進入),釋放CPU資源,進入休眠狀態,等待新的消息或者事件喚醒。
Linux pipe/epoll 機制是一種IO多路複用的機制,基於事件驅動,監測了多個文件描述符。即使沒有消息,如果有其他的操作(比如事件)喚醒了主線程,主線程就退出休眠的狀態,繼續工作,而消息Looper的繼續阻塞不會導致主線程卡死。
而在消息機制中,來了新的消息,也會調用一個本地方法喚醒休眠中的主線程
// MessageQueue.java
private native static void nativeWake(long ptr);
消息與隊列 Message/MessageQueue
引:
一個線程不僅只有一個Looper,並且也只有一個MessageQueue,這是如何保證的;
如果向隊列中發送大量消息,消息又一直在不斷被處理,那麼爲什麼不會頻繁GC Message導致內存抖動?
帶着這兩個問題來看源碼
線程MessageQueue唯一
// Looper.java
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 這裏創建並向ThreadLocal中存入了Looper實例
sThreadLocal.set(new Looper(quitAllowed));
}
// 構造方法中創建了MessageQueue實例
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
第一個問題已經有答案了:
因爲Looper由線程私有存儲ThreadLocal的哈希數組結構保證線程唯一,MessageQueue又是由Looper實例化的成員變量,那麼也是線程唯一的。
IdleHandler的作用
在MessageQueue中有個接口IdleHandler,顧名思義是一個空閒時處理任務的處理器。
public static interface IdleHandler {
boolean queueIdle();
}
這個接口提供了一個返回boolean值的方法。
在MessageQueue中有一個ArrayList<IdleHandler>的集合
此外,還提供了addIdle、removeIdle等方法用於向集合添加和移除IdleHandler。
那麼這個IdleHandler有什麼作用呢?在何時調用queueIdle方法?
在MessageQueue#next()中有了答案:
Message next() {
// 省略了nativePollOnce等邏輯
...
// 在阻塞結束(消息隊列中的消息執行完了,而隊列中下一個要執行的消息還沒有達到可執行的時間)時
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
} // 獲取列表中IdleHandler個數
if (pendingIdleHandlerCount <= 0) {
// 沒有IdleHandler直接進入下一次循環
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
// 在這裏調用了queueIdle方法!!!
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
}
也就是當消息隊列空閒時,可以執行IdleHandler的一些任務,這樣可以提升性能。
比如在onResume方法中,使用主線程的消息隊列的IdleHandler做一些準備工作,或者單純的想獲取UI繪製完成的回調,在繪製完成以後立即可以執行一些操作。
Message如何避免內存抖動——消息複用
在使用Message時,通常建議使用Message.obtain()系列的方法,這是有原因的。
當然也可以通過new Message() 來創建消息,只是更推薦上面的方式,並且new的方式最終也會被做一些響應處理。
幾個重要變量的含義:
// Message.java
public final class Message implements Parcelable {
...
Message next;// 消息單向鏈表的下一個消息對象
/** @hide */
public static final Object sPoolSync = new Object();// 同步鎖
private static Message sPool;// 私有靜態成員,消息池
private static int sPoolSize = 0;// 消息池的大小,即消息對象個數
private static final int MAX_POOL_SIZE = 50;// 消息池最多可容納50個消息對象
...
}
由此可以看出消息池最多可以服用的消息對象是50個,這個池是一個單向鏈表的數據結構;
從obtain()看消息複用
// Message.java
public static Message obtain() {
synchronized (sPoolSync) {// 同步鎖
if (sPool != null) {
// 當池可以複用時,取出單向鏈表隊頭的消息對象使用,
// 然後把隊頭重置爲next,並維護池中可用的消息對象的個數
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
// 消息池被清空了智能創建新的消息對象
return new Message();
}
答案就是,消息對象維護了一個消息池,由於消息本身是一種單向鏈表的結構,維護對頭來複用消息對象,並且在Looper.loop()方法中通過recycleUnchecked()方法來回收消息對象。
消息處理者Handler
Looper獲取了消息隊列中的消息後,通過Handler#dispathMessage()方法來分發給Handler,就會重發handleMessage方法,於是被重寫的handleMessage中的業務邏輯就被執行了。
延時消息是怎樣被入隊和分發處理的?
在日常使用中,經常使用sendMessageDelay/postDelay/sendMesageAtTime等方法,那麼延時消息是怎麼入隊的呢?
// Handler.java
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
首先,這些方法最終都是調用sendMessageAtTime實現的!
// Handler.java
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
...
return enqueueMessage(queue, msg, uptimeMillis);//
}
其次,直接調用了enqueueMessage進行入隊!這個方法最終調用了MessageQueue#enqueueMessage()方法
那麼是怎麼實現延時呢?
下面是簡化後的MessageQueue#enqueueMessage()方法
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
// 一些可用非空判斷
...
synchronized (this) {
// 在退出隊列的回收異常退出
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
// 標記入隊的消息inUse 獲取時間 獲取消息隊列對頭
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 當消息隊列中沒有消息或者新入隊的消息時間小或爲0,需要先執行入隊的新消息
// 變更了對頭
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 同步屏障有關省略
...
Message prev;
for (;;) {
prev = p;
p = p.next;
// 找到第一個時間比要入隊新消息的消息然後放入,或者沒有就放隊尾
// 實質上是消息隊列用when這個時間來排序了,時間小的在隊頭,從小到大的順序排列
// 新消息找到正確的入隊位置就退出此循環
if (p == null || when < p.when) {
break;
}
// 同步屏障有關省略
...
}
// 然後這一步就是真正的入隊
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {// 決定是否要喚醒主線程
nativeWake(mPtr);
}
}
return true;
}
答案有了:延時消息也是在觸發時就入隊了,只是消息隊列裏面會依據when這個時間標誌從小到大屏排序;
那怎樣保證延時處理呢?答案就在Looper.loop()方法中,阻塞的過程中有一個nextPollTimeoutMillis參數,如果消息隊列中隊頭就是一個延時消息,那麼loop在取消息的時候,會計算一個新的nextPollTimeoutMillis阻塞超時時間,來在delay的時間向Handler交付Message,於是就實現了延時消息!!!
題外話:Handler內存泄露的注意項
Handler在Activity中使用時常常要注意避免內存泄露,這裏泄露的對象就是Activity,常常在使用延時消息時,需要注意如果延時消息還沒有處理,Activity就銷燬了,那麼就會出問題。
Handler內存泄露的引用鏈
MainThread --> ThreadLocal<Looper> --> Looper --> MessageQueue --> Message --> Handler --> Activity
匿名內部類Handler持有Activity的引用,Message.target.dispatchMessage()持有了target(Handler)的引用,就是延時消息持有了Handler的引用,延時消息又enqueue在MessageQueue中,MessageQueue由是由Looper創建的,最終存放在主線程ThreadLocal中了。
解決方法
只要打破引用鏈的環節就解決了
- 讓Handler作爲靜態內部類或者外部類,不持有Activity引用,即可避免Activity泄露,但是此時沒有Activity的引用了,可以引入弱引用、軟引用;
- 讓Handler作爲普通內部類使用,但是在Activity生命週期結束時,移除消息隊列中的所有回調和消息;(即onDestroy中 調用Handler#removeCallbacksAndMessages一類的remove方法)