本篇內容主要講解“怎么掌握Handler消息機制”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“怎么掌握Handler消息機制”吧!
Handler 的基本原理
子線程中怎么使用 Handler
MessageQueue 獲取消息是怎么等待
為什么不用 wait 而用 epoll 呢?
線程和 Handler Looper MessageQueue 的關系
多個線程給 MessageQueue 發消息,如何保證線程安全
Handler 消息延遲是怎么處理的
View.post 和 Handler.post 的區別
Handler 導致的內存泄漏
非 UI 線程真的不能操作 View 嗎
代碼分析基于 Android SDK 28
大家可以先看上面的問題思考一下,如果都清楚的話,下面的文章也沒必要看了~
關于 Handler 的原理,相比不用多說了,大家都應該知道,一張圖就可以說明(圖片來自網絡)。

除了上面 Handler 的基本原理,子線程中如何使用 Handler 也是一個常見的問題。 子線程中使用 Handler 需要先執行兩個操作:Looper.prepare 和 Looper.loop。 為什么需要這樣做呢?Looper.prepare 和 Looper.loop 都做了什么事情呢? 我們知道如果在子線程中直接創建一個 Handler 的話,會報如下的錯誤:
"Can't create handler inside thread xxx that has not called Looper.prepare()
我們可以看一下 Handler 的構造函數,里面會對 Looper 進行判斷,如果通過 ThreadLocal 獲取的 Looper 為空,則報上面的錯誤。
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}那么 Looper.prepare 里做了什么事情呢?
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}可以看到,Looper.prepare 就是創建了 Looper 并設置給 ThreadLocal,這里的一個細節是每個 Thread 只能有一個 Looper,否則也會拋出異常。 而 Looper.loop 就是開始讀取 MessageQueue 中的消息,進行執行了。
這里一般會引申一個問題,就是主線程中為什么不用手動調用這兩個方法呢?相信大家也都明白,就是 ActivityThread.main 中已經進行了調用。 通過這個問題,又可以引申到 ActivityThread 相關的知識,這里就不細說了。
上面說到 Looper.loop 其實就是開始讀取 MessageQueue 中的消息了,那 MessageQueue 中沒有消息的時候,Looper 在做什么呢?我們知道是在等待消息,那是怎么等待的呢?
通過 Looper.loop 方法,我們知道是 MessageQueue.next() 來獲取消息的,如果沒有消息,那就會阻塞在這里,MessageQueue.next 是怎么等待的呢?
public static void loop() {
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
}
} Message next() {
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
// ...
}
}在 MessageQueue.next 里調用了 native 方法 nativePollOnce。
// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
// ...
mLooper->pollOnce(timeoutMillis);
// ...
}
// Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
// ...
result = pollInner(timeoutMillis);
// ...
}
int Looper::pollInner(int timeoutMillis) {
// ...
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
}從上面代碼中我們可以看到,在 native 側,最終是使用了 epoll_wait 來進行等待的。 這里的 epoll_wait 是 Linux 中 epoll 機制中的一環,關于 epoll 機制這里就不進行過多介紹了,大家有興趣可以參考 https://segmentfault.com/a/1190000003063859
那其實說到這里,又有一個問題,為什么不用 java 中的 wait / notify 而是要用 native 的 epoll 機制呢?
說起來 java 中的 wait / notify 也能實現阻塞等待消息的功能,在 Android 2.2 及以前,也確實是這樣做的。 可以參考這個 commit https://www.androidos.net.cn/android/2.1_r2.1p2/xref/frameworks/base/core/java/android/os/MessageQueue.java 那為什么后面要改成使用 epoll 呢?通過看 commit 記錄,是需要處理 native 側的事件,所以只使用 java 的 wait / notify 就不夠用了。 具體的改動就是這個 commit https://android.googlesource.com/platform/frameworks/base/+/fa9e7c05c7be6891a6cf85a11dc635a6e6853078%5E%21/#F0
Sketch of Native input for MessageQueue / Looper / ViewRoot MessageQueue now uses a socket for internal signalling, and is prepared to also handle any number of event input pipes, once the plumbing is set up with ViewRoot / Looper to tell it about them as appropriate. Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3
不過這里最開始使用的還是 select,后面才改成 epoll。 具體可見這個 commit https://android.googlesource.com/platform/frameworks/base/+/46b9ac0ae2162309774a7478cd9d4e578747bfc2%5E%21/#F16
至于 select 和 epoll 的區別,這里也不細說了,大家可以在上面的參考文章中一起看看。
這里的關系是一個線程對應一個 Looper 對應一個 MessageQueue 對應多個 Handler。
既然一個線程對應一個 MessageQueue,那多個線程給 MessageQueue 發消息時是如何保證線程安全的呢? 說來簡單,就是加了個鎖而已。
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ...
}
}Handler 引申的另一個問題就是延遲消息在 Handler 中是怎么處理的?定時器還是其他方法? 這里我們先從事件發起開始看起:
// Handler.java
public final boolean postDelayed(Runnable r, long delayMillis)
{
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
// 傳入的 time 是 uptimeMillis + delayMillis
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// ...
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 調用 MessageQueue.enqueueMessage
return queue.enqueueMessage(msg, uptimeMillis);
}從上面的代碼邏輯來看,Handler post 消息以后,一直調用到 MessageQueue.enqueueMessage 里,其中最重要的一步操作就是傳入的時間是 uptimeMillis + delayMillis。
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ...
msg.when = when;
Message p = mMessages; // 下一條消息
// 根據 when 進行順序排序,將消息插入到其中
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 找到 合適的節點
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
}
// 插入操作
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// 喚醒隊列進行取消息
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}通過上面代碼我們看到,post 一個延遲消息時,在 MessageQueue 中會根據 when 的時長進行一個順序排序。 接著我們再看看怎么使用 when 的。
Message next() {
// ...
for (;;) {
// 通過 epoll_wait 等待消息,等待 nextPollTimeoutMillis 時長
nativePollOnce(ptr, nextPollTimeoutMillis);
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) { // 說明需要延遲執行,通過; nativePollOnce 的 timeout 來進行延遲
// 獲取需要等待執行的時間
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else { // 立即執行的消息,直接返回
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// No more messages.
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);
}
// 執行 idle handlers 內容
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);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// 如果執行了 idle handlers 的內容,現在消息可能已經到了執行時間,所以這個時候就不等待了,再去檢查一下消息是否可以執行, nextPollTimeoutMillis 需要置為 0
nextPollTimeoutMillis = 0;
}
}通過上面的代碼分析,我們知道了執行 Handler.postDelayd 時候,會執行下面幾個步驟:
將我們傳入的延遲時間轉化成距離開機時間的毫秒數
MessageQueue 中根據上一步轉化的時間進行順序排序
在 MessageQueue.next 獲取消息時,對比當前時間(now)和第一步轉化的時間(when),如果 now < when,則通過 epoll_wait 的 timeout 進行等待
如果該消息需要等待,會進行 idel handlers 的執行,執行完以后會再去檢查此消息是否可以執行
我們最常用的 Handler 功能就是 Handler.post,除此之外,還有 View.post 也經常會用到,那么這兩個有什么區別呢? 我們先看下 View.post 的代碼。
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}通過代碼來看,如果 AttachInfo 不為空,則通過 handler 去執行,如果 handler 為空,則通過 RunQueue 去執行。 那我們先看看這里的 AttachInfo 是什么。 這個就需要追溯到 ViewRootImpl 的流程里了,我們先看下面這段代碼。
// ViewRootImpl.java
final ViewRootHandler mHandler = new ViewRootHandler();
public ViewRootImpl(Context context, Display display) {
// ...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
}
private void performTraversals() {
final View host = mView;
// ...
if (mFirst) {
host.dispatchAttachedToWindow(mAttachInfo, 0);
mFirst = false;
}
// ...
}代碼寫了一些關鍵部分,在 ViewRootImpl 構造函數里,創建了 mAttachInfo,然后在 performTraversals 里,如果 mFirst 為 true,則調用 host.dispatchAttachedToWindow,這里的 host 就是 DecorView,如果有讀者朋友對這里不太清楚,可以看看前面【面試官帶你學安卓-從View的繪制流程】說起這篇文章復習一下。
這里還有一個知識點就是 mAttachInfo 中的 mHandler 其實是 ViewRootImpl 內部的 ViewRootHandler。
然后就調用到了 DecorView.dispatchAttachedToWindow,其實就是 ViewGroup 的 dispatchAttachedToWindow,一般 ViewGroup 中相關的方法,都是去依次調用 child 的對應方法,這個也不例外,依次調用子 View 的 dispatchAttachedToWindow,把 AttachInfo 傳進去,在 子 View 中給 mAttachInfo 賦值。
// ViewGroup
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
// View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// ...
}看到這里,大家可能忘記我們開始剛剛要做什么了。
我們是在看 View.post 的流程,再回顧一下 View.post 的代碼:
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}現在我們知道 attachInfo 是什么了,是 ViewRootImpl 首次觸發 performTraversals 傳進來的,也就是觸發 performTraversals 之后,View.post 都是通過 ViewRootImpl 內部的 Handler 進行處理的。
如果在 performTraversals 之前或者 mAttachInfo 置為空以后進行執行,則通過 RunQueue 進行處理。
那我們再看看 getRunQueue().post(action); 做了些什么事情。
這里的 RunQueue 其實是 HandlerActionQueue。
HandlerActionQueue 的代碼看一下。
public class HandlerActionQueue {
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
}通過上面的代碼我們可以看到,執行 getRunQueue().post(action); 其實是將代碼添加到 mActions 進行保存,然后在 executeActions 的時候進行執行。
executeActions 執行的時機只有一個,就是在 dispatchAttachedToWindow(AttachInfo info, int visibility) 里面調用的。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}看到這里我們就知道了,View.post 和 Handler.post 的區別就是:
如果在 performTraversals 前調用 View.post,則會將消息進行保存,之后在 dispatchAttachedToWindow 的時候通過 ViewRootImpl 中的 Handler 進行調用。
如果在 performTraversals 以后調用 View.post,則直接通過 ViewRootImpl 中的 Handler 進行調用。
這里我們又可以回答一個問題了,就是為什么 View.post 里可以拿到 View 的寬高信息呢? 因為 View.post 的 Runnable 執行的時候,已經執行過 performTraversals 了,也就是 View 的 measure layout draw 方法都執行過了,自然可以獲取到 View 的寬高信息了。
這個問題就是老生常談了,可以由此再引申出內存泄漏的知識點,比如:如何排查內存泄漏,如何避免內存泄漏等等。
我們使用 Handler 最多的一個場景就是在非主線程通過 Handler 去操作 主線程的 View。 那么非 UI 線程真的不能操作 View 嗎? 我們在執行 UI 操作的時候,都會調用到 ViewRootImpl 里,以 requestLayout 為例,在 requestLayout 里會通過 checkThread 進行線程的檢查。
// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
mThread = Thread.currentThread();
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}我們看這里的檢查,其實并不是檢查主線程,是檢查 mThread != Thread.currentThread,而 mThread 指的是 ViewRootImpl 創建的線程。 所以非 UI 線程確實不能操作 View,但是檢查的是創建的線程是否是當前線程,因為 ViewRootImpl 創建是在主線程創建的,所以在非主線程操作 UI 過不了這里的檢查。
一個小小的 Handler,其實可以引申出很多問題,這里這是列舉了一些大家可能忽略的問題,更多的問題就等待大家去探索了~ 這里來總結一下:
一張圖解釋(圖片來自網絡) 
Looper.prepare 創建 Looper 并添加到 ThreadLocal 中
Looper.loop 啟動 Looper 的循環
通過 epoll 機制進行等待和喚醒。
在 Android 2.2 及之前,使用 Java wait / notify 進行等待,在 2.3 以后,使用 epoll 機制,為了可以同時處理 native 側的消息。
一個線程對應一個 Looper 對應一個 MessageQueue 對應多個 Handler。
通過對 MessageQueue 加鎖來保證線程安全。
將傳入的延遲時間轉化成距離開機時間的毫秒數
MessageQueue 中根據上一步轉化的時間進行順序排序
在 MessageQueue.next 獲取消息時,對比當前時間(now)和第一步轉化的時間(when),如果 now < when,則通過 epoll_wait 的 timeout 進行等待
如果該消息需要等待,會進行 idel handlers 的執行,執行完以后會再去檢查此消息是否可以執行
View.post 最終也是通過 Handler.post 來執行消息的,執行過程如下:
如果在 performTraversals 前調用 View.post,則會將消息進行保存,之后在 dispatchAttachedToWindow 的時候通過 ViewRootImpl 中的 Handler 進行調用。
如果在 performTraversals 以后調用 View.post,則直接通過 ViewRootImpl 中的 Handler 進行調用。
略過不講~
不能操作,原因是 ViewRootImpl 會檢查創建 ViewRootImpl 的線程和當前操作的線程是否一致。而 ViewRootImpl 是在主線程創建的,所以非主線程不能操作 View。
到此,相信大家對“怎么掌握Handler消息機制”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。