深入理解Android訊息機制
二、Android的訊息機制
Android的訊息機制主要說的是Handler的執行機制,Handler的執行需要MessageQueue和Looper,MessageQueue就是訊息佇列,它是採用單鏈表的資料結構來儲存訊息列表。而Looper會不斷的檢視MessageQueue中是否有訊息,當有訊息的時候就取出來。Hanlder在建立的時候會採用當前執行緒的Looper來構造訊息迴圈系統,但是預設的執行緒是沒有Looper的,比如我們在子執行緒建立一個Hanlder(這裡只是便於演示所以不採用執行緒池了,建議標準開發中使用執行緒池)
/** * 子執行緒使用Hanlder測試方法 */ private void ThreadHandlerTest() { new Thread(new Runnable() { @Override public void run() { Handler handler = new Handler(); } }).start(); }
執行結果報錯如下:
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:206)
at android.os.Handler.<init>(Handler.java:119)
at lonbon.com.hanlderlooperdemo.MainActivity$1.run(MainActivity.java:24)
at java.lang.Thread.run(Thread.java:764)
這隻因為預設的執行緒中沒有Looper,所以我們要為當前執行緒建立Looper物件:
Looper.prepare(); Handler handler = new Handler(); Looper.loop();
我們通過Looper.prepare()方法建立Looper物件,.loop()方法開啟訊息迴圈。關於looper詳細的下面講解。
那麼我們可能有疑問了,我們在主執行緒中使用Handler的時候沒有建立looper物件也可以正常使用,那是因為預設的UI執行緒建立的時候預設建立了looper物件。
建立完Handler之後,通過handler的send或者post方法傳送訊息,這個訊息會被儲存到訊息佇列,Looper發現訊息佇列中有新的訊息便會處理這個訊息,然後handlermessage方法或者Runable方法會被呼叫,大致過程如圖所示。

在這裡插入圖片描述
三、ThreadLocal
ThreadLocal是Looper中的特殊概念,用來在當前執行緒中儲存資料,我們獲取當前執行緒的Looper也是通過ThreadLocal操作的,當然,日常開發中我們能使用的ThreadLocal的地方並不多。比如我們在兩個不同執行緒中進行如下操作:
首先我們宣告一個String型別的ThreadLocal變數,建立兩個執行緒分別使用set方法賦值,然後列印。
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
/** * 測試執行緒1 */ private void ThreadTest1() { new Thread(new Runnable() { @Override public void run() { threadLocal.set("BigYellow"); Log.d(TAG,threadLocal.get()); } }).start(); }
/** * 測試執行緒2 */ private void ThreadTest2() { new Thread(new Runnable() { @Override public void run() { threadLocal.set("大黃"); Log.d(TAG,threadLocal.get()); } }).start(); }
執行列印,日誌如下:
02-12 10:21:37.961 11719-12135/? D/TAG: BigYellow
02-12 10:21:37.966 11719-12136/? D/TAG: 大黃
我們可以看到取出的分別是各自執行緒對應的值,如果我們在主執行緒中呢?顯然是null因為我們沒有在主執行緒中存值。
接下來我們從原始碼的角度來分析ThreadLocal的存取值過程,首先我們看set方法。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
首先通過getMap方法獲取當前執行緒的ThreadLocalMap,如果map不為空就通過map的set方法將值儲存,如果為空則建立map,
我們來看下ThreadLocalMap,ThreadLocalMap是一個儲存當前執行緒資料的Map集合,set方法原始碼如下所示:
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
首先定義了一個Entry型別的陣列,我們主要來看for迴圈中的操作,for迴圈主要做的就是為插入值得位置找到合適的位置,通過不斷到table陣列中去尋找,直到存放的entry為null
if (k == key) { e.value = value; return; }
如果key的值相同說明該執行緒曾經設定過Threadlocal,直接賦值即可。
if (k == null) { replaceStaleEntry(key, value, i); return; }
Entry繼承的是WeakReference,這是弱引用帶來的坑
所以要判斷是否為null,如果為null就進行置換操作,即
replaceStaleEntry(key, value, i);
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
當table陣列中儲存的ThreadLocal對應的值還在但是key不存在了,就認為Entry過期了,
int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i;
上述程式碼檢查髒資料,清理整個table,否則會因為GC問題導致很嚴重的後果。
if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; }
如果找到key了我們需要進行替換,將過期資料進行刪除重新整理。原始碼中註釋的很清楚了,這裡就不一一解釋了。
個人感覺和之前早期版本(6.0之前)的set方法變化很大。
我們接下來來看ThreadLocal的get方法,首先同樣的獲取當前執行緒的ThreadLocalMap,獲取map的entry物件,如果不為空的話就從中取值即可。如果map為空就回到setInitialValue初始化方法.
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(); }
四、Looper
Looper我們上面說了是用來構建訊息迴圈系統,我們通過ThreadLocal來獲取當前執行緒的Looper物件.我們上面也說到了如何在子執行緒中建立looper,通過Looper的prepare方法為當前執行緒建立一個looper,通過loop方法開啟訊息迴圈。在主執行緒中建立Looper是通過
Looper.prepareMainLooper();
方法,因為UI執行緒的Looper比較特殊是預設建立好的,所以我們可以通過下列程式碼來獲取主執行緒的looper
Looper.getMainLooper();
我們可以開啟looper肯定也可以關閉looper,關閉looper有這個方法,一個是
getMainLooper().quit();
public void quit() { mQueue.quit(false); }
quit方法會直接退出looper,另一種方法是
getMainLooper().quitSafely();
和quit方法不同的是quitSafely方法呼叫後在訊息佇列中的訊息處理完成之後在退出,就像方法名一樣是安全退出。所以如果我們在子執行緒中手動建立了looper,記得在執行完執行緒後呼叫退出方法,否則子執行緒會一直處於等待狀態,影響效能。
接下來我們看looper是如何通過loop方法開啟訊息迴圈的,loop方法原始碼如下所示:
/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */ public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } // This must be in a local variable, in case a UI event sets the logger final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; final long traceTag = me.mTraceTag; if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); final long end; try { msg.target.dispatchMessage(msg); end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } if (slowDispatchThresholdMs > 0) { final long time = end - start; if (time > slowDispatchThresholdMs) { Slog.w(TAG, "Dispatch took " + time + "ms on " + Thread.currentThread().getName() + ", h=" + msg.target + " cb=" + msg.callback + " msg=" + msg.what); } } if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); } }
從中我們可以看到looper會不斷的呼叫
queue.next()
方法從訊息佇列中取出訊息,如果為空就一直等待,如果有訊息就呼叫
msg.target.dispatchMessage(msg);
方法處理msg.target就是傳送這條訊息的Handler物件,而handler的dispatchMessage方法又是在建立Handler所在的Looper執行的,所以這樣就將訊息交給指定的執行緒去處理了。
五、訊息佇列MessageQueue與Handler
訊息佇列MessageQueue主要有插入和讀取兩個操作,讀取成功後也就相當於刪除。
插入方法對應的enqueueMessage原始碼如下:
boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); } 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; } msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked; } else { // Inserted within the middle of the queue.Usually we don't have to wake // up the event queue unless there is a barrier at the head of the queue // and the message is the earliest asynchronous message in the queue. 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; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { nativeWake(mPtr); } } return true; }
我們上面也說了訊息佇列其實是一個單鏈表,所以就相當於單鏈表的插入操作。讀取方法是next方法,讀取後將訊息移除,這裡就不作過多解釋了。
而我們日常開發中最常用的就是建立Hanlder的匿名內部類方式(這種方式記得處理記憶體洩漏),然後通過hanlder.send方法傳送訊息,而send方法最終又會呼叫sendMessageAtTime方法,原始碼如下:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) { MessageQueue queue = mQueue; if (queue == null) { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); return false; } return enqueueMessage(queue, msg, uptimeMillis); }
我們看最終返回值就可以看到其實呼叫handler的send方法就是呼叫enqueueMessage方法往訊息佇列中插入了一條訊息,然後不斷迴圈的looper進去取出又交給handler處理,這樣就構成了Android的訊息機制。
上文原始碼基於Android8.0,如有紕漏歡迎指出探討。