深入理解Android訊息機制
在日常的開發中,Android 的訊息機制作為系統執行的根本機制之一,顯得十分的重要。
從 Handler 傳送訊息開始
檢視原始碼,Handler的post、send方法最終都會走到
sendMessageDelayed 會走到
這裡可以設定 Message 為非同步訊息
檢視 queue 的 enqueueMessage 方法, 我們剝離出核心程式碼:
如果是新的佇列頭,直接插入佇列
如果佇列裡面已經有訊息了,執行如下邏輯
插入訊息的時候,一般不會喚醒訊息佇列。如果訊息是非同步的,並且佇列頭不是一個非同步訊息的時候,會喚醒訊息佇列
訊息佇列的具體喚醒過程我們暫時不細看。把關注點移到 Looper 上。looper在執行的時候具體執行了什麼邏輯呢?檢視 Looper.java 的 looper() 方法
looper 方法中有一個死迴圈, 在死迴圈中,會獲取下一個 Message
當存在一個 barrier 訊息的時候,會尋找佇列中下一個非同步任務。而不是按照順序。 例如3個訊息,1,2,3, 2 是非同步訊息。如果不存在barrier的時候,next的順序就是 1,2,3 但是如果存在barrier的時候,則是 2,1,3
這裡如果 next 的 Message 不為空,就返回,並且將它移出佇列 在 MessageQueue 為空的時候,會順便去處理一下 add 過的 IdleHandler, 處理一些不重要的訊息
檢視 IdleHandler 的原始碼。
當 queueIdle() 為 false 的時候,會將它從 mIdleHandlers 中 remove,仔細思考下,我們其實可以利用IdleHandler實現不少功能, 例如
我們可以在 queueIdle 中,趁著沒有訊息要處理,統計一下頁面的渲染時間(訊息傳送完了說明UI已經渲染完了),或者算一下螢幕是否長時間沒操作等等。
拿到 Message 物件後,會將 Message 分發到對應的 target 去
檢視原始碼
當 msg 的 callback 不為 null 的時候,即通過 post(Runnable) 傳送資訊的會執行 handlerCallback(msg) 方法。如果 mCallback 不為 null並且 handleMessage 的結果為 false,則執行 handleMessage 方法。否則會停止分發。
檢視 handlerCallback 方法原始碼, callback 會得到執行。到這裡基本的Android訊息機制就分析完了,簡而言之就是,Handler 不斷的將Message傳送到一 根據時間進行排序的優先佇列裡面,而執行緒中的 Looper 則不停的從MQ裡面取出訊息,分發到相應的目標Handler執行。
為什麼主執行緒不卡?
分析完基本的訊息機制,既然 Looper 的 looper 方法是一個for(;;;)迴圈,那麼新的問題提出來了。為什麼Android會在主執行緒使用死迴圈?執行死迴圈的時候為什麼主執行緒的阻塞沒有導致CPU佔用的暴增?�
繼續分析在原始碼中我們沒有分析的部分:
會發現訊息佇列還是和native層有關係,繼續檢視android/platform/frameworks/base/core/jni/android_os_MessageQueue_nativeInit.cpp中nativeInit的實現:
這裡會發現我們初始化了一個 NativeMessageQueue ,檢視這個訊息佇列的建構函式
這裡會發現在mq中初始化了 native 的 Looper 物件,檢視android/platform/framework/native/libs/utils/Looper.cpp中 Looper 物件的建構函式
這裡我們會發現,在 native 層建立了一個epoll,並且對 epoll 的 event 事件進行了監聽。
什麼是epoll
在繼續分析原始碼之前,我們先分析一下,什麼是epoll
epoll是Linux中的一種IO多路複用方式,也叫做event-driver-IO。
Linux的select 多路複用IO通過一個select()呼叫來監視檔案描述符的陣列,然後輪詢這個陣列。如果有IO事件,就進行處理。
select的一個缺點在於單個程式能夠監視的檔案描述符的數量存在最大限制,select()所維護的儲存大量檔案描述符的資料結構,隨著檔案描述符數量的增大,其複製的開銷也線性增長。
epoll在select的基礎上(實際是在poll的基礎上)做了改進,epoll同樣只告知那些就緒的檔案描述符,而且當我們呼叫epoll_wait()獲得就緒檔案描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個陣列中依次取得相應數量的檔案描述符即可。
另一個本質的改進在於epoll採用基於事件的就緒通知方式(設定回撥)。在select中,程式只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個檔案描述符,一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,當程式呼叫epoll_wait()時便得到通知
關於epoll和select,可以舉一個例子來表達意思。select的情況和班長告訴全班同學交作業類似,會挨個去詢問作業是否完成,如果沒有完成,班長會繼續詢問。
而epoll的情況則是班長詢問的時候只是統計了待交作業的人數,然後告訴同學作業完成的時候告訴把作業放在某處,然後喊一下他。然後班長每次都去這個地方收作業。
大致瞭解了epoll之後,我們繼續檢視nativePollOnce方法,同理,會呼叫native Looper的pollOnce方法
在pollOnce中,會先處理沒有callback的response(ALOOPER_POLL_CALLBACK = -2),處理完後會執行pollInner方法
看到這裡,我們其實可以看出來整體訊息模型由 native 和 Java 2層組成,2層各自有自己的訊息系統。 Java層通過呼叫 pollonce 來達到呼叫底層epoll 讓死迴圈進入阻塞休眠的狀態,以避免浪費CPU, 所以這也解釋了為什麼Android Looper的死迴圈為什麼不會讓主執行緒CPU佔用率飆升。
java層和native層的對應圖如下:
備註
在很多時候,我們可以遇到這2個問題。既然看了 Handler 的原始碼,那麼,我們就順便分析一下這 2 個問題。
檢視Handler的構造方法,無參構造方法最後會呼叫
可以看到,這裡會直接獲取Looper
這裡會把每個 Looper 存到相應的ThreadLocal物件中,如果子執行緒直接建立了Handler,Looper 就會是一個null,所以會直接跑出一個"Can't create handler inside thread that has not called Looper.prepare()"的RuntimeException
那麼我們是何時把Looper放入ThreadLocal物件的呢?可以在Looper.prepare()中找到答案
這也解釋了,在每個 Thread 中,只會存在一個 Looper 物件。如果我們想在子執行緒中正常建立 Handler,就需要提前執行當前執行緒的 Looper,呼叫
就不會丟擲異常了。
總結
訊息機制作為 Android 的基礎,還是非常有深入瞭解的必要。對於我們遇到Handler傳送訊息的時候跑出的系統異常的排查也很有意義。
特別感謝
本次原始碼的閱讀過程中,遇到了很多不瞭解的問題例如epoll,這裡非常感謝IO哥(檢視IO哥大佬)助和指導。讓我在某些細節問題上暫時繞過和恍然大悟。
從 Handler 傳送訊息開始
檢視原始碼,Handler的post、send方法最終都會走到
public final boolean sendMessageDelayed(Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }
sendMessageDelayed 會走到
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
這裡可以設定 Message 為非同步訊息
檢視 queue 的 enqueueMessage 方法, 我們剝離出核心程式碼:
if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked; }
如果是新的佇列頭,直接插入佇列
如果佇列裡面已經有訊息了,執行如下邏輯
needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } msg.next = p; // invariant: p == prev.next prev.next = msg;
插入訊息的時候,一般不會喚醒訊息佇列。如果訊息是非同步的,並且佇列頭不是一個非同步訊息的時候,會喚醒訊息佇列
if (needWake) { nativeWake(mPtr); }
訊息佇列的具體喚醒過程我們暫時不細看。把關注點移到 Looper 上。looper在執行的時候具體執行了什麼邏輯呢?檢視 Looper.java 的 looper() 方法
looper 方法中有一個死迴圈, 在死迴圈中,會獲取下一個 Message
for (;;) { Message msg = queue.next(); // might block }
if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous());
當存在一個 barrier 訊息的時候,會尋找佇列中下一個非同步任務。而不是按照順序。 例如3個訊息,1,2,3, 2 是非同步訊息。如果不存在barrier的時候,next的順序就是 1,2,3 但是如果存在barrier的時候,則是 2,1,3
if (msg != null) { if (now < msg.when) { // Next message is not ready. Set a timeout to wake up when it is ready. 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; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; }
這裡如果 next 的 Message 不為空,就返回,並且將它移出佇列 在 MessageQueue 為空的時候,會順便去處理一下 add 過的 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 的原始碼。
* Callback interface for discovering when a thread is going to block * waiting for more messages. */ public static interface IdleHandler { /** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */ boolean queueIdle(); }
當 queueIdle() 為 false 的時候,會將它從 mIdleHandlers 中 remove,仔細思考下,我們其實可以利用IdleHandler實現不少功能, 例如
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { return false } });
我們可以在 queueIdle 中,趁著沒有訊息要處理,統計一下頁面的渲染時間(訊息傳送完了說明UI已經渲染完了),或者算一下螢幕是否長時間沒操作等等。
拿到 Message 物件後,會將 Message 分發到對應的 target 去
msg.target.dispatchMessage(msg);
檢視原始碼
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
當 msg 的 callback 不為 null 的時候,即通過 post(Runnable) 傳送資訊的會執行 handlerCallback(msg) 方法。如果 mCallback 不為 null並且 handleMessage 的結果為 false,則執行 handleMessage 方法。否則會停止分發。
private static void handleCallback(Message message) { message.callback.run(); }
檢視 handlerCallback 方法原始碼, callback 會得到執行。到這裡基本的Android訊息機制就分析完了,簡而言之就是,Handler 不斷的將Message傳送到一 根據時間進行排序的優先佇列裡面,而執行緒中的 Looper 則不停的從MQ裡面取出訊息,分發到相應的目標Handler執行。
為什麼主執行緒不卡?
分析完基本的訊息機制,既然 Looper 的 looper 方法是一個for(;;;)迴圈,那麼新的問題提出來了。為什麼Android會在主執行緒使用死迴圈?執行死迴圈的時候為什麼主執行緒的阻塞沒有導致CPU佔用的暴增?�
繼續分析在原始碼中我們沒有分析的部分:
- 訊息佇列構造的時候是否呼叫了jni部分
- nativeWake、nativePollOnce這些方法的作用是什麼
MessageQueue(boolean quitAllowed) { mQuitAllowed = quitAllowed; mPtr = nativeInit(); }
會發現訊息佇列還是和native層有關係,繼續檢視android/platform/frameworks/base/core/jni/android_os_MessageQueue_nativeInit.cpp中nativeInit的實現:
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); return reinterpret_cast<jlong>(nativeMessageQueue); }
這裡會發現我們初始化了一個 NativeMessageQueue ,檢視這個訊息佇列的建構函式
NativeMessageQueue::NativeMessageQueue() : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) { mLooper = Looper::getForThread(); if (mLooper == NULL) { mLooper = new Looper(false); Looper::setForThread(mLooper); } }
這裡會發現在mq中初始化了 native 的 Looper 物件,檢視android/platform/framework/native/libs/utils/Looper.cpp中 Looper 物件的建構函式
// 簡化後的程式碼 Looper::Looper(bool allowNonCallbacks) : mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) { int wakeFds[2]; int result = pipe(wakeFds); mWakeReadPipeFd = wakeFds[0]; mWakeWritePipeFd = wakeFds[1]; result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK); result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK); mEpollFd = epoll_create(EPOLL_SIZE_HINT); struct epoll_event eventItem; memset(& eventItem, 0, sizeof(epoll_event)); eventItem.events = EPOLLIN; eventItem.data.fd = mWakeReadPipeFd; result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem); }
這裡我們會發現,在 native 層建立了一個epoll,並且對 epoll 的 event 事件進行了監聽。
什麼是epoll
在繼續分析原始碼之前,我們先分析一下,什麼是epoll
epoll是Linux中的一種IO多路複用方式,也叫做event-driver-IO。
Linux的select 多路複用IO通過一個select()呼叫來監視檔案描述符的陣列,然後輪詢這個陣列。如果有IO事件,就進行處理。
select的一個缺點在於單個程式能夠監視的檔案描述符的數量存在最大限制,select()所維護的儲存大量檔案描述符的資料結構,隨著檔案描述符數量的增大,其複製的開銷也線性增長。
epoll在select的基礎上(實際是在poll的基礎上)做了改進,epoll同樣只告知那些就緒的檔案描述符,而且當我們呼叫epoll_wait()獲得就緒檔案描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個陣列中依次取得相應數量的檔案描述符即可。
另一個本質的改進在於epoll採用基於事件的就緒通知方式(設定回撥)。在select中,程式只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個檔案描述符,一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,當程式呼叫epoll_wait()時便得到通知
關於epoll和select,可以舉一個例子來表達意思。select的情況和班長告訴全班同學交作業類似,會挨個去詢問作業是否完成,如果沒有完成,班長會繼續詢問。
而epoll的情況則是班長詢問的時候只是統計了待交作業的人數,然後告訴同學作業完成的時候告訴把作業放在某處,然後喊一下他。然後班長每次都去這個地方收作業。
大致瞭解了epoll之後,我們繼續檢視nativePollOnce方法,同理,會呼叫native Looper的pollOnce方法
while (mResponseIndex < mResponses.size()) { const Response& response = mResponses.itemAt(mResponseIndex++); int ident = response.request.ident; if (ident >= 0) { int fd = response.request.fd; int events = response.events; void* data = response.request.data; if (outFd != NULL) *outFd = fd; if (outEvents != NULL) *outEvents = events; if (outData != NULL) *outData = data; return ident; } }
在pollOnce中,會先處理沒有callback的response(ALOOPER_POLL_CALLBACK = -2),處理完後會執行pollInner方法
// 移除了部分細節處理和日誌程式碼 // 新增了分析原始碼的日誌 int Looper::pollInner(int timeoutMillis) { if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) { nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime); if (messageTimeoutMillis >= 0 && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) { timeoutMillis = messageTimeoutMillis; } } // Poll. int result = ALOOPER_POLL_WAKE; mResponses.clear(); mResponseIndex = 0; struct epoll_event eventItems[EPOLL_MAX_EVENTS]; // 等待事件發生或者超時 int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); // Acquire lock. mLock.lock(); // Check for poll error. // epoll 事件小於0, 發生錯誤 if (eventCount < 0) { if (errno == EINTR) { goto Done; } result = ALOOPER_POLL_ERROR; goto Done; } if (eventCount == 0) { // epoll事件為0,超時,直接跳轉到Done result = ALOOPER_POLL_TIMEOUT; goto Done; } //迴圈遍歷,處理所有的事件 for (int i = 0; i < eventCount; i++) { int fd = eventItems[i].data.fd; uint32_t epollEvents = eventItems[i].events; if (fd == mWakeReadPipeFd) { if (epollEvents & EPOLLIN) { awoken(); //喚醒,讀取管道里面的事件 } else { } } else { ssize_t requestIndex = mRequests.indexOfKey(fd); if (requestIndex >= 0) { int events = 0; // 處理request,生成response物件,push到相應的Vector pushResponse(events, mRequests.valueAt(requestIndex)); } else { } } } Done: ; // Invoke pending message callbacks. // 發生超時的邏輯處理 mNextMessageUptime = LLONG_MAX; while (mMessageEnvelopes.size() != 0) { // 處理Native端的Message nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0); if (messageEnvelope.uptime <= now) { // Remove the envelope from the list. // We keep a strong reference to the handler until the call to handleMessage // finishes. Then we drop it so that the handler can be deleted *before* // we reacquire our lock. { // obtain handler sp<MessageHandler> handler = messageEnvelope.handler; Message message = messageEnvelope.message; mMessageEnvelopes.removeAt(0); mSendingMessage = true; mLock.unlock(); handler->handleMessage(message); // 處理訊息事件 } // release handler mLock.lock(); mSendingMessage = false; result = ALOOPER_POLL_CALLBACK; // 設定回撥 } else { // The last message left at the head of the queue determines the next wakeup time. mNextMessageUptime = messageEnvelope.uptime; break; } } // Release lock. mLock.unlock(); // Invoke all response callbacks. // 執行回撥 for (size_t i = 0; i < mResponses.size(); i++) { Response& response = mResponses.editItemAt(i); if (response.request.ident == ALOOPER_POLL_CALLBACK) { int fd = response.request.fd; int events = response.events; void* data = response.request.data; int callbackResult = response.request.callback->handleEvent(fd, events, data); if (callbackResult == 0) { removeFd(fd); //移除fd } // Clear the callback reference in the response structure promptly because we // will not clear the response vector itself until the next poll. response.request.callback.clear(); // 清除reponse引用的回撥方法 result = ALOOPER_POLL_CALLBACK; // 發生回撥 } } return result; }
看到這裡,我們其實可以看出來整體訊息模型由 native 和 Java 2層組成,2層各自有自己的訊息系統。 Java層通過呼叫 pollonce 來達到呼叫底層epoll 讓死迴圈進入阻塞休眠的狀態,以避免浪費CPU, 所以這也解釋了為什麼Android Looper的死迴圈為什麼不會讓主執行緒CPU佔用率飆升。
java層和native層的對應圖如下:
備註
- Java 層和 native 層通過 MessageQueue 裡面持有一個 native 的MessageQueue 物件進行互動。WeakMessageHandler 繼承自MessageHandler,NativeMessageQueue 繼承自 MessageQueue
- Java 層和 native 層實質是各自維護了一套相似的訊息系統。C層發出的訊息和Java層發出的訊息可以沒有任何關係。所以 Framework 層只是很巧的利用了底層 epoll 的機制達到阻塞的目的。
- 通過 pollOnce 的分析,可以發現訊息的處理其實是有順序的,首先是處理native message,然後處理native request,最後才會執行java層,處理java層的message
在很多時候,我們可以遇到這2個問題。既然看了 Handler 的原始碼,那麼,我們就順便分析一下這 2 個問題。
檢視Handler的構造方法,無參構造方法最後會呼叫
public Handler(Callback callback, boolean async) { mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; }
可以看到,這裡會直接獲取Looper
public static @Nullable Looper myLooper() { return sThreadLocal.get(); }
這裡會把每個 Looper 存到相應的ThreadLocal物件中,如果子執行緒直接建立了Handler,Looper 就會是一個null,所以會直接跑出一個"Can't create handler inside thread that has not called Looper.prepare()"的RuntimeException
那麼我們是何時把Looper放入ThreadLocal物件的呢?可以在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)); }
這也解釋了,在每個 Thread 中,只會存在一個 Looper 物件。如果我們想在子執行緒中正常建立 Handler,就需要提前執行當前執行緒的 Looper,呼叫
Looper.prepare()
就不會丟擲異常了。
總結
訊息機制作為 Android 的基礎,還是非常有深入瞭解的必要。對於我們遇到Handler傳送訊息的時候跑出的系統異常的排查也很有意義。
特別感謝
本次原始碼的閱讀過程中,遇到了很多不瞭解的問題例如epoll,這裡非常感謝IO哥(檢視IO哥大佬)助和指導。讓我在某些細節問題上暫時繞過和恍然大悟。
來自: github
相關文章
- 深入理解 Android 訊息機制原理Android
- 理解 Android 訊息機制Android
- 深入理解Android非同步訊息處理機制Android非同步
- 深入探索Android訊息機制之HandlerAndroid
- 深入VCL 理解BCB的訊息機制 (一) (轉)
- Android訊息機制Message訊息池Android
- android訊息機制—HandlerAndroid
- Android訊息機制HandlerAndroid
- Android 之訊息機制Android
- Android的訊息機制Android
- android之 Android訊息機制Android
- Android訊息傳遞之Handler訊息機制Android
- android 訊息傳遞機制進階EventBus的深入探究Android
- Android 深入理解 Notification 機制Android
- Android非同步訊息機制Android非同步
- Android訊息機制Handler用法Android
- Android 訊息機制詳解Android
- Android訊息機制(七) RxjavaAndroidRxJava
- 分析與理解訊息反射機制 (轉)反射
- Android訊息機制-ThreadLocal原理解析:資料存取Androidthread
- OC訊息機制,訊息轉發機制
- [Android進階]Android訊息機制Android
- Android非同步訊息機制-深入理解Handler、Looper和MessageQueue之間的關係Android非同步OOP
- 深入淺出 Runtime(三):訊息機制
- 如何生動形象的理解Android Handler訊息處理機制Android
- Android Handler 訊息機制詳述Android
- Android的Handler訊息機制 解析Android
- Android訊息機制原始碼分析Android原始碼
- 訊息機制
- Android 訊息機制詳解(Android P)Android
- 深入理解Android訊息佇列原理篇Android佇列
- 全面剖析Android訊息機制原始碼Android原始碼
- 淺析Android中的訊息機制Android
- Android程式間通訊–訊息機制及IPC機制實現薦Android
- iOS訊息機制iOS
- SAP訊息機制
- Android 訊息機制:Handler、MessageQueue 和 LooperAndroidOOP
- 由外到內——剖析Android訊息機制Android