前言
Handler不管是作為一種消息機制,還是作為切換線程的手段,在Android中都有充足的應用場景。在不了解Handler原理的情況下,僅知道上層API如何發送消息,如何處理消息,加之了解一些Handler容易造成的問題以及應對策略,實際上也不會造成什么大問題。
Handler的戲份比上面所描述的要重,因為一個APP的運行過程,是不斷接受消息以及處理消息的過程。比如Activity,從啟動、創建、生命周期回調、銷毀,都是借由Handler發送消息來驅動完成。從一個APK的安裝,到一個View的更新,都離不開Handler的幫助。
如果對于以下問題有疑問,那這篇文章可能有借鑒價值:
- Handler如何保證運行在目標線程
- Handler容易造成內存泄漏的原因
- loop()為什么不會阻塞,CPU為什么不會忙等
- MessageQueue如何存儲
- Message如何緩存
- 什么是線程空閑消息
- 線程如何使用Handler機制
note: 本文源碼版本為8.0
Handler 如何運行
Handler角色分配:
Handler中存在四種角色
Handler
Handler用來向Looper發送消息,在Looper處理到對應的消息時,Handler在對消息進行具體的處理。上層關鍵API為handleMessage(),由子類自行實現處理邏輯。
Looper
Looper運行在目標線程里,不斷從消息隊列MessageQueue讀取消息,分配給Handler處理。Looper起到連接的作用,將來自不同渠道的消息,聚集在目標線程里處理。也因此Looper需要確保線程唯一。
MessageQueue
存儲消息對象Message,當Looper向MessageQueue獲取消息,或Handler向其插入數據時,決定消息如何提取、如何存儲。不僅如此,MessageQueue還維護與Native端的連接,也是解決Looper.loop() 阻塞問題的 Java 端的控制器。
Message
Message包含具體的消息數據,在成員變量target中保存了用來發送此消息的Handler引用。因此在消息獲得這行時機時,能知道具體由哪一個Handler處理。此外靜態成員變量sPool,則維護了消息緩存池以復用。
運行過程
首先,需要構建消息對象。獲取消息對象從Handler.obtainMessage()系列方法可以獲取Message,這一系列的函數提供了相應對應于Message對象關鍵成員變量對應的函數參數,而無論使用哪一個方法獲取,最終通過Message.obtain()獲取具體的Message對象。
// 緩存池
private static Message sPool;
// 緩存池當前容量
private static int sPoolSize = 0;
// 下一節點
Message next;
public static Message obtain() {
// 確保同步
synchronized (sPoolSync) {
if (sPool != null) {
// 緩存池不為空
Message m = sPool;
// 緩存池指向下一個Message節點
sPool = m.next;
// 從緩存池拿到的Message對象與緩存斷開連接
m.next = null;
m.flags = 0; // clear in-use flag
// 緩存池大小減一
sPoolSize--;
return m;
}
}
// 緩存吃沒有可用對象,返回新的Message()
return new Message();
}
Message成員變量中存在類型為Message的next,可以看出Message為鏈表結構,而上面代碼從緩存池里獲取消息對象的過程可以用下圖描述:
創建出消息之后,通過Handler將消息發送到消息隊列,發送方法有很多,不一一陳列。發送發上有兩種:
- 將Message對象發送到
- 發送Runnable,通過getPostMessage()將Runnable包裝在Message里,表現為成員變量callback
private static Message getPostMessage(Runnable r) {
// 獲取Message
Message m = Message.obtain();
// 記住Runnale,等消息獲得執行時回調
m.callback = r;
return m;
}
不管哪種方式發送,最終消息隊列MessageQueue只知接受到了消息對象Message。而將消息加入到消息隊列,最終通過enqueueMessage()加入。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// Message.target 記住 Handler 以明確是由哪一個Handler來處理這個消息的
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
// 消息入隊
return queue.enqueueMessage(msg, uptimeMillis);
}
在將消息加入消息隊列時,有時需要提供延遲信息delayTime,以期未來多久后執行,這個值存于 uptimeMillis。
之后,等待Looper輪詢從消息隊列中讀取消息進行處理。見Looper.loop()
public static void loop() {
// 拿到Looper
final Looper me = myLooper();
if (me == null) {
// 沒調用prepare初始化Looper,報錯
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// 拿到消息隊列
final MessageQueue queue = me.mQueue;
......
for (;;) {
// 從消息隊列取出下一個信息
Message msg = queue.next();
if (msg == null) {
// 消息為空,返回
return;
}
.......
try {
// 分發消息到Handler
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
}
// 消息回收,放入緩存池
msg.recycleUnchecked();
}
Looper從MessageQueue里取出Message,Message.target則是具體的Hander,Handler.dispatchMessage()將觸發具體分配邏輯。此后,將Message回收,放入緩存池。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 這個情況說明了次消息為Runnable,觸發Runnable.run()
handleCallback(msg);
} else {
if (mCallback != null) {
// 指定了Handler的mCallback
if (mCallback.handleMessage(msg)) {
return;
}
}
// 普通消息處理
handleMessage(msg);
}
}
Handler分配消息分三種情況:
- 可以通過Handler發送Runnable消息到消息隊列,因此handleCallback()處理這種情況
- 可以給Handler設置Callback,當分配消息給Handler時,Callback可以優先處理此消息,如果Callback.handleMessage()返回了true,不再執行Handler.handleMessage()
- Handler.handleMessage()處理具體邏輯
回收則是通過Message.recycleUnchecked()
void recycleUnchecked() {
// 這里是將Message各種屬性重置操作
......
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
// 緩存池還能裝下,回收到緩存池
// 下面操作將此Message加入到緩存池頭部
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
通過上面的分析,Handler的運行如下圖
- Handler 從緩存池獲取Message,發送到MessageQueue
- Looper不斷從MessageQueue讀取消息,通過Message.target.dispatchMessage()觸發Handler處理邏輯
- 回收Message到緩存池
目前來看,可以算是了解了Handler的運行機制,但是對于解答開篇提出的問題,捉襟見肘,需要深入Handler。
Java端與Native端建立連接
實際上,不僅僅是Java端存在Handler機制,在Native端同樣存在Handler機制。他們通過MessageQueue建立了連接。
一般來說,Looper通過prepare()進行初始化
private static void prepare(boolean quitAllowed) {
// 保證Looper在線程唯一
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 將Looper放入ThreadLocal
sThreadLocal.set(new Looper(quitAllowed));
}
在實例化Looper時,需要確保Looper在線程里是唯一的。Handler知道自己的具體Looper對象,而Looper運行在具體的線程里并在此線程里處理消息。這也是為什么Looper能達到切換線程的目的。Looper線程唯一需要ThreadLocal來確保,ThreadLocal的原理,簡單來說Thread里有類型為ThreadLocalMap的成員threadLocals,通過ThreadLocal能將相應對象放入threadLocals里通過K/V存儲,如此能保證變量在線程范圍內存儲,其中Key為ThreadLocal< T > 。
private Looper(boolean quitAllowed) {
// 初始化MessageQueue
mQueue = new MessageQueue(quitAllowed);
// 記住當前線程
mThread = Thread.currentThread();
}
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
// 與Native建立連接
mPtr = nativeInit();
}
在MessageQueue創建時,通過native方法nativeInit()與Native端建立了連接,mPtr為long型變量,存儲一個地址。
方法實現文件位于frameworks/base/core/jni/android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);
// 返回給Java層的mPtr, NativeMessageQueue地址值
return reinterpret_cast<jlong>(nativeMessageQueue);
}
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
// 檢查Looper 是否創建
if (mLooper == NULL) {
mLooper = new Looper(false);
// 確保Looper唯一
Looper::setForThread(mLooper);
}
}
在Native端創建了NativeMessageQueue,同樣也創建了Native端的Looper。在創建NativeMessageQueue后,將它的地址值返回給了Java層MessageQueue.mPtr。實際上,Native端Looper實例化時做了更多事情。
Nativ端Looper文件位于system/core/libutils/Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
// 添加到epoll的文件描述符,線程喚醒事件的fd
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s",
strerror(errno));
AutoMutex _l(mLock);
rebuildEpollLocked();
}
void Looper::rebuildEpollLocked() {
.....
// Allocate the new epoll instance and register the wake pipe.
// 創建epolle實例,并注冊wake管道
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));
struct epoll_event eventItem;
// 清空,把未使用的數據區域進行置0操作
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
// 監聽可讀事件
eventItem.events = EPOLLIN;
// 設置作為喚醒評判的fd
eventItem.data.fd = mWakeEventFd;
// 將喚醒事件(mWakeEventFd)添加到epoll實例,意為放置一個喚醒機制
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",
strerror(errno));
// 添加各種事件的fd到epoll實例,如鍵盤、傳感器輸入等
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
request.fd, strerror(errno));
}
}
}
初次見面,上面的代碼讓人迷糊,因為缺少了關鍵的知識點,epoll機制。
如何理解epoll機制?
文件、socket、pipe(管道)等可以進行I/O操作的對象可以視為流。既然是I/O操作,則有read端讀入數據,有write端寫入數據。但是兩端并不知道對方進行操作的時機。而epoll則能觀察到哪個流發生了了I/O事件,并進行通知。
這個過程,就好比你在等快遞,但你不知道快遞什么時候來,那這時你可以去睡覺,因為你知道快遞送來時一定會打個電話叫醒你,讓你拿快遞,接著做你想的事情。
epoll有效地降低了CPU的使用,在線程空間時令其休眠,等有事件到來時再講它喚醒。
epoll機制可以參考這里
在知道了epoll之后,再來看上面的代碼,就可以理解了。在Native端創建Looper時,會創建用來喚醒線程的fd —— mWakeEventFd,創建epoll實例并注冊管道,清空管道數據,監聽可讀事件。當有數據寫入mWakeEventFd描述的文件時,epoll能監聽到此事件,并通知將目標線程喚醒。
在Java端MessageQueue.mPrt存儲了Native端NativeMassageQueue的地址,可以利用NativeMassageQueue享用此機制。
發送數據的具體過程
之前說過,Handler發送消息時,最終通過MessageQueue.enqueueMessage向消息隊列中插入消息,下面為具體代碼
boolean enqueueMessage(Message msg, long when) {
......
synchronized (this) {
......
// 記錄消息處理的時間
msg.when = when;
Message p = mMessages;
// 喚醒線程的標志位
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 這里三種情況:
// 1、目標消息隊列是空隊列
// 2、插入的消息處理時間等于0
// 3、插入的消息處理時間小于保存在消息隊列頭的消息處理時間
// 這三種情況都插入列表頭
msg.next = p;
mMessages = msg;
// mBlocked 表示當前線程是否睡眠
needWake = mBlocked;
} else {
// 這里則說明消息處理時間大于消息列表頭的處理時間,因此需要找到合適的插入位置
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 這里的循環是找到消息的插入位置
for (;;) {
prev = p;
p = p.next;
// 到鏈表尾,或處理時間早于p的時間
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
// 如果插入的消息在目標隊列中間,是不需要檢查改變線程喚醒狀態的
needWake = false;
}
}
// 插入到消息隊列
msg.next = p;
prev.next = msg;
}
if (needWake) {
// 喚醒線程
nativeWake(mPtr);
}
}
return true;
}
消息隊列里的消息也是以鏈表形式存儲,存儲順序則按照處理的時間順序。那么在向消息隊列里插入數據時,存在四種情況:
- 目標消息隊列是空隊列
- 插入的消息處理時間等于0
- 插入的消息處理時間小于保存在消息隊列頭的消息處理時間
- 插入的消息處理時間大于消息隊列頭的消息處理時間
前三種情況,將消息插入消息隊列頭的位置,在這種情況下,因為不能保證當前消息是否達到了可以處理的狀態,且如果此時線程是睡眠的,則需要調用nativeWake()將其線程喚醒。后一種情況,則需要找到消息的插入位置,因不影響線程狀態而需要改變線程狀態。
插入消息如圖
mPtr保存了NativeMessageQueue的地址,所以Native可以知道具體操作的NativeMessageQueue,當前用它來喚醒線程,實際調用鏈為
MessageQueue.cpp.nativeWake()
-> MessageQueue.cpp.wake()
-> Looper.cpp.wake()
void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif
uint64_t inc = 1;
// 向管道寫入一個新數據,這樣管道因為發生了IO事件被喚醒
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d: %s",
mWakeEventFd, strerror(errno));
}
}
}
實現也簡單,向mWakeEventFd文件里寫入一個數據,根據epoll機制監聽到此次I/O事件,將線程喚醒。
消息讀取
Looper不斷從MessageQueue讀取消息進行處理,通過MessageQueue.next()進行讀取。
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
// 獲取NativeMessageQueue地址失敗,無法正常使用epoll機制
return null;
}
// 用來保存注冊到消息隊列中的空閑消息處理器(IdleHandler)的個數
int pendingIdleHandlerCount = -1;
// 如果這個變量等于0,表示即便消息隊列中沒有新的消息需要處理,當前
// 線程也不要進入睡眠等待狀態。如果值等于-1,那么就表示當消息隊列中沒有新的消息
// 需要處理時,當前線程需要無限地處于休眠等待狀態,直到它被其它線程喚醒為止
int nextPollTimeoutMillis = 0;
for (;;) {
......
// 檢查當前線程的消息隊列中是否有新的消息需要處理,嘗試進入休眠
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 當前時間
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
// mMessages 表示當前線程需要處理的消息
Message msg = mMessages;
if (msg != null && msg.target == null) {
// 找到有效的Message
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 {
// 有消息需要處理
// 不要進入休眠
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
// 指向下一個需要處理的消息
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// 沒有更多消息,休眠時間無限
nextPollTimeoutMillis = -1;
}
......
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
// 獲取IdleHandler數
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// 沒有IdleHandler需要處理,可直接進入休眠
mBlocked = true;
continue;
}
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; // release the reference to the handler
boolean keep = false;
try {
// 處理對應邏輯,并由自己決定是否保持激活狀態
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
// 不需要存活,移除
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// 重置IdleHandler數量
pendingIdleHandlerCount = 0;
/**
* 這里置0,表示下一次循環不能馬上進入休眠狀態,因為IdleHandler在處理事件的時間里,
* 有可能有新的消息發送來過來,需要重新檢查。
*/
nextPollTimeoutMillis = 0;
}
}
分為兩種情況處理:
取到消息Message時
需要查看當前時間是否達到了Message處理的時間,如果達到了則返回,改變mMessages指向下一消息。如果沒達到,則計算要達到處理的時間,還需要休眠多久,并進行休眠
沒有更多Message時
當消息隊列里沒有消息時,則會檢查是否有IdleHandler需要處理。在Handler機制里,允許添加一些事件,在線程空閑時進行處理,表現為IdleHandler,可以通過MessageQueue.addIdleHandler添加。當有IdleHandler需要處理,則在IdleHandler處理完后,線程不能馬上進入休眠狀態,在此期間可能已有新消息加入消息隊列,需要重新做檢查。如果沒有IdleHandler,則可以進入休眠。
線程休眠調用鏈為
NativeMessageQueue.nativePollOnce()
-> NativeMessageQueue.pollOnce()
-> Looper.pollOnce()
-> Looper.pollInner()
int Looper::pollInner(int timeoutMillis) {
......
// 這個是用來監聽實例化時創建的epoll實例的文件描述符的IO讀寫事件
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 如果沒有事件,進入休眠,timeoutMillis為休眠事件
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
......
/**
* 檢測是哪一個文件描述符發生了IO讀寫事件
*/
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd) {
if (epollEvents & EPOLLIN) {
// 如果文件描述符為mWakeEventFd,并且讀寫事件類型為EPOLLIN,說明
// 當前線程所關聯的一個管道被寫入了一個新的數據
// 喚醒
awoken();
}
}
......
}
}
Java層提供了線程休眠時間timeoutMillis,通過epoll_wait()讓線程進行休眠。當線程被喚醒后,查看文件描述符,如果為mWakeEventFd并且為I/O事件,則說明當當前線程所關聯的一個管道被寫入了一個新的數據,通過awoken()處理。當前線程已是喚醒狀態,awoken()則是將管道中的數據讀出達到清理目的,但并不關心數據什么。核心目的是喚醒線程。
總結
Handler機制更具體的原理如圖:
- Looper通過prepare()創建,借助ThreadLocal保證線程唯一,如果沒有進行prepare(),調用Loop()會拋出異常
- Looper在實例化時創建MessageQueue,MessageQueue與NativeMessageQueue建立連接,NativeMessageQueue存儲地址存于MessageQueue.mPtr。Native端也建立了Handler機制,使用epoll機制。Java端借由NativeMessageQueue能達到使用epoll機制的目的
- 從Message緩存里獲取Message,緩存為鏈表存儲,從頭出取出,并且Message在回收時也是插入頭部。如果存緩存里取不到,則新建
- Handler向MessageQueue插入消息,如果消息插入消息隊列頭部,需要喚醒線程;如果插入消息隊列中,無需改變線程狀態
- Looper.loop() 不斷從消息隊列獲取消息,消息隊列獲取消息時會出現兩種情況。如果取到消息,但沒達到處理時間,則讓線程休眠;如果沒有更多消息,則在處理IdleHandler事后,在考慮讓線程進入休眠
- Message達到了可處理狀態,則有Handler處理,處理時考慮三種情況,消息內容為Runnable時、設置了Handle.Callback時、普通消息時,對應調用為Message.callback.run() 、 Callback.handleMessage()、Handler.handleMessage()
- 從Handler機制里,epoll可以簡單理解為,當Handler機制沒有消息要處理時,讓線程進入休眠,當Handler機制有消息要處理時,將線程喚起。通過Native端監聽mWakeEventFd的I/O事件實現
答疑
這里對文章一開始的問題進行回答
Handler如何保證運行在目標線程
Looper在實例化時通過ThreadLocal保證線程唯一。Looper運行在目標線程中,接收Handler發送的消息并進行處理。Message創建時與具體的Handler進行了關聯,因此能知道由哪一個Handler進行相應。
Handler容易造成內存泄漏的原因
Message.target存有Handler的引用,以知道自身由哪一個Handler來處理。因此,當Handler為非靜態內部類、或持有關鍵對象的其它表現形式時(如Activity常表現為Context),就引用了其它外部對象。當Message得不到處理時,被Handler持有的外部對象會一直處于內存泄漏狀態。
loop()為什么不會阻塞,CPU為什么不會忙等
通過epoll機制監聽文件I/O時間,在有Message需要處理時,寫入數據以喚醒線程;在沒有Message要處理時,讓線程進入休眠狀態。
MessageQueue如何存儲
以鏈表存儲,MessageQueue.mMessages指向頭節點。
Message如何緩存
以鏈表緩存,取出時從頭部取出,回收時插入頭部。
什么是線程空閑消息
Handler提供的一種機制,允許添加IdleHandler事件。并在沒有更多Message要處理,要進入休眠前,讓IdleHandler做具體事情,也就是線程空間時處理的事件。
子線程如何使用Handler機制
只要保證在子線程先執行Looper.prepare()再使用Looper.Loop()即可,但實際應用場景不多。 順便提一句,主線程初始化Looper操作在ActivityTread.main()里觸發,簡單了解即可。
參考
《Android 系統源代碼情景分析》 —— 第十三章
Android Handler機制10之Native的實現
我讀過的最好的epoll講解--轉自”知乎“