您好,登錄后才能下訂單哦!
本篇內容主要講解“怎么掌握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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。