從原始碼一次徹底理解Android的訊息機制
情景重現
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
textView.setText("after changed" );
}
});
開頭我們就看到了如上的一段簡單的偽碼。因為這裡我試圖去還原一種場景,一種可能我們不少人最初接觸Android時可能都會遇到的錯誤場景。
這種場景的邏輯很簡單:在程式執行中,某個控制元件的值會被動態的改變。這個值通過某種途徑獲取,但該途徑是耗時的(例如訪問網路,檔案讀寫等)。
上面的偽碼中的邏輯是:點選按鈕將會改變文字框的值,且這個值是通過某種耗時的操作獲取到,於是我們通過將執行緒休眠5秒來模擬這個耗時操作。
好的,現在我們通過編譯正式開始執行類似的程式碼。那麼,我們首先會收到熟悉的一個錯誤,即“Application No Response(ANR)”。
接著,通過查閱相關的資料,我們明白了:原來我們像上面這樣做時,耗時操作直接就是存在於主執行緒,即所謂的UI執行緒當中的。
那麼這就代表著:這個時候的UI執行緒會因為執行我們的耗時操作而被堵塞,也自然就無法響應使用者其它的UI操作。於是,就會引起ANR這個錯誤了。
現在我們瞭解了ANR出現的原因,所以我們自然就會通過一些手段來避開這種錯誤。我們決定將耗時的操作從UI執行緒拿走,放到一個新開的子執行緒中:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
textView.setText("after changed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
});
好的,現在我們模擬的耗時操作已經被我們放到了UI執行緒之外的執行緒。當我們信心十足的再次執行程式,確得到了如下的另一個異常資訊:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
從異常資訊中,我們看到系統似乎是在告訴我們一個資訊,那就是:只有建立一個檢視層次結構的原始執行緒才能觸控到它的檢視。
那麼,我們似乎就能夠理解這種異常出現的原因了:我們將耗時操作放在了我們自己建立的分執行緒中,顯然它並非原始執行緒,自然就不能夠去訪問View。
這樣設計的初衷實際上是不難猜想的,如果任何執行緒都能去訪問UI,請聯想一下併發程式設計中各種不可預知且操蛋的問題,可能我們的介面最終就熱鬧了。
但是,現在我們針對於這一異常的解決方案似乎也不難給出了。既然只有主執行緒能夠訪問View,那麼我們只需要將更新UI的操作放到主執行緒就OK了。
那麼,這裡就順帶一提了。不知道有沒有人和我曾經一樣,想當然的寫出過類似下面一樣的程式碼:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 這是在主執行緒裡執行的
textView.setText("after changed");
}
});
是的,如上的程式碼十分的,非常的“想當然”。這當然是因為對Java多執行緒機制理解不夠所造成的。更新UI的操作確實是放到了主執行緒,但是!!!:
這並不代表著,UI更新一定會在分執行緒的耗時操作全部完成後才會執行,這自然是因為執行緒執行權是隨機切換的。也就是說,很可能出現的情況是:
分執行緒中的耗時操作現在並沒有執行完成,即我們還沒有得到一個正確的結果,便切換到了主執行緒執行UI的更新,這個時候自然就會出現錯誤。
Handler粉墨登場
這個時候,作為菜鳥的我們有點不知所措。於是,趕緊上網查查資料,看看有沒有現成的解決方案吧。這時,通常“Handler”就會進入我們的視線了:
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0x0001:
textView.setText("after changed");
}
}
};
//===============================================
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
mHandler.sendEmptyMessage(0x0001);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
我們發現關於Handler的使用似乎十分容易不過,容易到當我們認為自己掌握了它的時候似乎都沒有成就感:
- 首先,我們只需要建立一個Handler物件。
- 接著,我們會在需要的地方,通過該Handler物件傳送指定的Message。
- 最後,該Handler物件通過handleMessage方法處理接收到的Message。
但我們沉下心來想一想:Handler為什麼能夠解決我們之前碰到的非原始執行緒不能更新UI的錯誤呢?它的實現原理如何?它能做的就只是更新UI嗎?
掰扯了這麼多,帶著這些疑問,我們終於來到了我們這篇blog最終的目的,那就是搞清楚Android的訊息機制(主要就是指Handler的執行機制)。
從建構函式切入
就像醫生如果要弄清楚人體構造,方式當然是通過解剖來進行研究。而我們要研究一個物件的實現原理,最好的方式就是通過分析它的原始碼。
個人的習慣是,當我們沒有一個十分明確的切入點的時候,選擇建構函式切入通常是比較合適的,那我們現在就開啟Handler的建構函式來看一下:
// 1.
public Handler() {
this(null, false);
}
// 2.
public Handler(Callback callback) {
this(callback, false);
}
// 3.
public Handler(Looper looper) {
this(looper, null, false);
}
// 4.
public Handler(Looper looper, Callback callback) {
this(looper, callback, false);
}
// 5.
public Handler(boolean async) {
this(null, async);
}
// 6.
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
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;
}
// 7.
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
好的,分析一下我們目前所看的,我覺得我們至少可以很容易的分析並掌握兩點:
- Handler自身提供了7種構造器,但實際上只有最後兩種提供了具體實現。
- 我們發現各種構造器最終圍繞了另外兩個類,即Callback與Looper。我們推測它們肯定不是做擺設的。
現在我們來分別看一下唯一兩個提供了具體實現的構造器,我們發現:
除了 ”if (FIND_POTENTIAL_LEAKS) “這一段看上去和反射有關的if程式碼塊之外,這兩個構造器剩下的實現其實基本上是完全一致的,即:
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
唯一的不同在於mLooper這一例項變數的賦值方式:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
//==========================================
mLooper = looper;
“mLooper = Looper.myLooper();”這種方式究竟有何神奇,我們這裡暫且不提。我們的注意力聚焦在以上看到的幾個例項變數,開啟原始碼看看:
final MessageQueue mQueue;
final Looper mLooper;
final Callback mCallback;
final boolean mAsynchronous;
mAsynchronous是一個布林型的變數,並且我們看到預設情況它的構造值是false,從命名我們就不難推測到,它多半與非同步有關。除此之外:
其餘型別分別是”MessageQueue,Looper,Callback“。一定記住它們!!!正是它們配合Handler及Message完成了整個訊息傳遞的架構。
OK,首先我們來看Callback這個東西,從命名來看絕逼與回撥有關係,開啟原始碼,果不其然正是定義在Handler內部的一個介面:
public interface Callback {
public boolean handleMessage(Message msg);
}
我們看到了其中唯一宣告的一個方法介面,看上去似乎有點眼熟。是的,那麼它與Handler自身的handleMessage有何聯絡?我們暫且提不提。
現在,我們再接著看Looper和MessageQueue兩個型別。很遺憾的是,這裡我們發現:這是另外單獨定義的兩個全新的類。也就是說:
目前我們似乎無法在邏輯上將其與Handler聯絡起來。我們現在只知道從命名上來說,它們似乎分別代表著“迴圈”與“訊息佇列”的意思。
post()與sendMessage系列函式
那麼,到了這一步似乎情況有點糟糕,因為似乎失去了下一步的切入點。沒關係,這個時候我們回憶一下我們通常怎麼樣使用Handler:
- mHandler.post();
- mHandler.sendMessage();
沒錯,我們基本上就是通過以上兩種方式去使用Handler。所以現在我們開啟這兩個方法相關的原始碼來看看:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}
由此我們發現的是:post函式最終呼叫的仍是send系列的函式;而sendMessage底部也依然是通過sendMessageDelayed呼叫的。
並且!檢視一系列的send方法原始碼發現:它們最終都將通過sendMessageAtTime來完成整個呼叫。所以顯然這將是我們下一個關注點。
先分析一下post方法的實現,我們看到其實Handler內部是通過getPostMessage對我們傳入的Runnable物件進行了一次封裝。
當我們看到getPostMessage方法的實現,我們會發現沒什麼大不了的,只是將傳入Runnable物件賦值給了一個Message物件而已。
但我們也可能會觀察到另外一點。就是我們可能會在使用Message時會通過構造器得到訊息物件,而這裡是通過靜態方法obtain。
使用obtainMessage而非構造器
這二者有什麼不同呢?我們先開啟Message的構造器的方法來看一下:
/** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
*/
public Message() {
}
好的,我們發現構造器實際上沒有任何實現內容。而註釋告訴我們:更推薦使用obtain系列的方法來獲取一個Message物件。
那麼我們就好奇了?為什麼更推薦使用obtain呢?我們以無參的obtain方法為例,開啟原始碼瞧一瞧:
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
從以上程式碼我們可以看到的是:obtain最終的本質仍是產生Message物件。關鍵在於一個叫sPool的東西,這兄弟到底是個什麼鬼呢?
實際上是Handler內部會通過這個叫做sPool的靜態全域性變數構建一個類似“池”的東西,而通過next屬性我們不難推斷”池”應該是以單鏈表來實現的。
再檢視方法的註釋:從全域性池中返回一個新的訊息例項。使我們能夠避免在許多情況下分配新的物件。由此我們好像已經知道為何推薦obtain了。
包括網上很多打著類似“new message與obtainMessage之間區別”的資料裡,一大段的文字之後,我們會發現最終實際有用的就類似一句話:
obtainMessage可以從池中獲取Message物件,從而避免新的物件建立,達到節約記憶體的效果。但這樣當然還是熄滅不了一顆好奇的心:
究竟為什麼這個“池”能夠避免新的物件建立呢?要解開這個疑問,我們還需要關注Handler類中的另一個方法“recycleUnchecked”的如下程式碼:
void recycleUnchecked() {
// 第一部分
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
// 第二部分
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
該方法顧名思義,主要的工作通常主要就是回收一個完成使命的Message物件。而這個回收動作發生的時機是什麼呢?
通常來說,我們可以通過人為呼叫msg.recycle()來完成回收;另一種更常見的回收時機發生在MessageQuene當中,我們稍後會看到。
接下來我們該方法中的回收工作都做了什麼,程式碼中註釋為第一部分的程式碼做的工作很易懂,就是將回收的message物件的各個屬性清空。
第二部分其實就是將回收的物件向“池”內新增的過程,而之前說到的obtain方法,其一旦判斷sPoll不為null,就直接從池內獲取閒置物件,不再建立。
到此實際上我們就已經分析了,為什麼obtain能夠節約記憶體開銷的原理了。但如果你的資料結構和我一樣渣,可能還會有點暈。沒關係,看如下程式碼:
Message msg1 = Message.obtain();
msg1.recycle();
Message msg2 = Message.obtain();
我們對應於這三行簡單的程式碼,來有代入感的分析一下它們執行的過程,相信就會有個比較清晰的理解了。
- 首先獲取msg1的時候,這個時候sPool肯定是為null的。所以它的工作實際與直接通過構造器建立物件沒有區別。
- 通過msg1物件呼叫recycle方法,最終進入之前所說的回收工作的第二部分執行。此時的結果為:msg1.next = sPoll(即null,沒有next節點);sPoll = msg1;
- 這時我們再通過obtain去獲取物件msg2,進入方法後,判斷sPoll不為null。於是, Message m = msg1;注意:
這代表我們已經從池中取出了msg1,於是執行sPool = m.next時,我們說到msg1.next是null,所以sPool再次等於null,邏輯完全正確。
與此同時,我們也可以想得到,假設m.next不等於null時:sPool = m.next的邏輯實際上就轉換成了,將sPool指向next節點,即代表我們已經取走一個物件了,池將指向下一個節點,即為我們下次要獲取的訊息物件。
MessageQuene 訊息佇列的工作機制
好了,現在相信我們都清楚以上的概念了。我們的關注點將回到我們之前提到的關鍵位置,即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);
}
我們發現該方法的實現很簡單,但最終會呼叫另一個方法“enqueueMessage”,趕緊開啟這個方法看一看:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
我們發現該方法顯然使用了委託設計模式,將最終的方法實現委託了給了quene物件,即MessageQuene來實現。
對於MessageQuene中的enqueueMessage方法,該方法的原始碼個人覺得沒必要全部關注。我們先看下面這小段程式碼:
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.recycle方法,記得我們之前說過的回收工作嗎?這裡正是另一種發生時機,這個時機的標準如上所示,正是:
“mQuitting”為true,而在什麼時候mQuitting會被設定為true,我們稍後將會看到,這裡先暫且一提。接著看另一端更為關鍵的程式碼:
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;
有了之前的基礎,我們發現該方法所做的工作實際上很簡單,它仍然是以單鏈表的形式,通過不斷追加next節點達到向佇列中新增Message的效果。
由此,我們發現:當我們通過handler物件post或send了一條訊息,其實最終的工作很簡單,就是向MessageQuene即訊息佇列中追加一條訊息而已。
那麼,接下來呢?自然的,訊息追加到了隊列當中。我們則需要從佇列中依次取出訊息物件,才能對其作出處理。苦苦尋覓一番之後:
我們發現了next()方法,該方法的實現歸根結底是通過迴圈來不斷的從佇列中拉取訊息,考慮到篇幅,我們不再貼出原始碼。唯一注意:
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);
}
當沒有新的訊息來臨之前,如上的程式碼將能夠確保佇列“阻塞”而一直等待新的訊息物件來臨。好了,我們總結一下:
MessageQuene將通過enqueueMessage方法向佇列中插入訊息,而通過next方法取出訊息。但現在的關鍵點在:
關於enqueueMessage方法我們已經知道它在Handler當中被呼叫,而next方法目前我們只看到宣告,還沒看到呼叫的產生。
Looper - 訊息的拉取者
以next()方法的呼叫為關鍵字按圖索驥,我們最終發現它在我們之前提到的另一個關鍵的東西”Lopper”中產生了呼叫,具體是Lopper中的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;
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
msg.target.dispatchMessage(msg);
...
...
msg.recycleUnchecked();
}
}
我們只保留了關於loop方法最為關鍵的部分,我們依次來分析一下,首先我們注意到的,肯定是一個名為“myLooper()”的方法呼叫:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
ThreadLocal顯神通
我們從程式碼中看到邏輯十分簡單清晰,就是通過myLooper()來獲取looper物件,而最終的方式則是通過sThreadLocal來獲取。
這裡,就不得不提到一個功能強大的東西ThreadLocal。我們來看一下Looper的原始碼當中關於sThreadLocal的定義:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
這個東西的作用究竟如何?簡單來說,我們知道普通的定義一個例項變數,它將建立在“堆”上。
而“堆”並非執行緒私有的,所以例項變數也將被執行緒共享。而ThreadLocal則是將變數的作用域限制為執行緒私有。舉例來說:
static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();
sThreadLocal.set("1");
new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal.set("2");
}
}).start();
上面的程式碼通過sThreadLocal.get()來獲取string,在主執行緒中和我們new的執行緒當中獲取的值是獨立的,分別是“1”和“2”。
接下來,我們看到的就是將會在一個無限迴圈中一直通過呼叫MessageQuene的next()方法來獲取訊息物件。
假設此次獲取到了msg物件,則會通過msg.target呼叫dispatchMessage方法來分發訊息。問題在於target是個什麼東西?
在Message類中檢視原始碼,我們可以知道target自身是一個Handler型別的物件。但通常我們都沒有人為的去為這個變數賦值。
那麼這個變數通常預設是什麼呢?回到之前Handler類的enqueneMessage方法當中,看到如下程式碼:
msg.target = this;
也就是說,如果我們沒有明確的去為Message物件的target域賦值,它將被預設賦值為傳送這條Message的Handler物件自身。
那麼,我們先要做的就簡單了,回到Handler類當中,檢視dispatchMessage方法的原始碼如下:
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,那麼將通過handleCallback方法來處理訊息(實際上就是msg.callback.run());
- 否則進一步判斷mCallback是否為null,不為null則通過mCallback.handleMessage來處理訊息。
- 最後如果mCallback也為null,則會呼叫Handler自身的handleMessage方法來處理訊息。
邏輯很簡單,我們唯一注意的就是msg.callback和mCallback這兩兄弟是指什麼東西?很簡單:
msg.callback是我們通過Message.obtain(Handler h, Runnable callback)或者通過Handler.post(Runnable r)傳入的Runnable物件。
而mCallback就更熟悉了,回想我們之前檢視Handler的構造器時看到的東西。
mCallback的本質就是Handler內部定義的介面Callback,所以通過它實際就是通過回撥介面處理訊息。
而這裡,我覺得值得一說的是msg.callback這個東西。我們知道當它不為null,最終實際將通過message.callback.run()來處理訊息。
也就是說最終實際上是呼叫了Runnable物件的run方法,但有Java的基礎就會知道這樣的呼叫實際與執行緒無關,只相當於普通的呼叫一個例項方法而已。
對於這點,我們一定要有清楚的認識,否則可能會因為使用不當造成一些想不到的錯誤。具體的例子我們暫且不說,放在最後的總結部分來看。
實際上到了這裡,我們就已經把構建Android訊息機制的四個關鍵,即Handler,Message,MessageQuene及Looper給聯絡起來了。簡單總結一下:
- 通過呼叫Handler的post或者send系列的方法來發送一條Message。
- 這一條Message最終會加入到連結串列結構的MessageQuene當中存放。
- Looper會通過內部的loop方法不斷呼叫MessageQuene的next()方法獲取下一條Message
- 當Looper獲取到Message方法後,又會通過Handler的dispatchMessage來分發並處理訊息。
Looper的正確建立
我相信到了這裡,我們或多或少都會有些收穫。但對於剛接觸Andoid訊息機制的朋友來說,還可能存在一個疑問,那就是:
通過之前我們的分析與理解,我們知道了對於Handler處理訊息的機制來說,Lopper的參與是至關重要的。
但與此同時,我們發現之前我們似乎並沒有建立Looper。我們不免會考慮,是系統幫助我們建立了嗎?答案是肯定的。
回憶一下之前的程式碼,我們是通過無參的構造器來建立Handler物件的。我們也可以看到,該構造器最終會呼叫我們之前說到的第6個構造器。
然後我們發現在第6種構造器當中,是通過“mLooper = Looper.myLooper();”的方式來獲取looper物件的。
這時我們就想起了之前的ThreadLocal,但即使是使用ThreadLocal,也起碼得有一個ThreadLocal.set(Looper)的過程吧。
這個過程是在什麼時候完成的呢?正常來說,我們推斷這個過程很可能發生在Looper的構造器中。但一番查詢我們發現Looper壓根沒提供公有的構造器。
經過苦苦的尋覓之後,最終我們會發現在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));
}
好了,現在我們回想一下,我們之前的程式碼中建立的Handler,是一個屬於當前Activity的例項域。這代表它的建立是在主執行緒中完成的。
而關於Looper的建立的確也是在Android的主執行緒,即ActivityThread中完成建立的。具體的呼叫位於主執行緒的入口main方法中。
並且,主執行緒裡的Looper,是通過呼叫Looper類專門為主執行緒建立Looper物件封裝的方法“prepareMainLooper()”來建立的。
現在我們再看關於”Can’t create handler inside thread that has not called Looper.prepare()”的這個異常,我們就很容易找到原因了。
因為通過原始碼我們知道這個異常就是在第6個構造器中,當通過Looper.myLooper()獲取結果為null時報告的。
同時,我們知道我們在主執行緒建立Handler的時候,沒有問題是因為主執行緒預設就建立了Looper物件。那麼:
當我們要在主執行緒以外的執行緒中建立Handler物件的時候,只要我們也為它建立對應的Looper物件就行了。示例如下:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler();
Looper.loop();
}
}).start();
是的,在建立完物件後,別忘了呼叫loop()方法,因為這才是開啟迴圈從訊息佇列中獲取訊息的關鍵。
好了,現在我們正式接著看loop()方法中接下來的程式碼msg.recycleUnchecked();。是的,我們很熟悉的”回收”,由此我們可以知道:
回收Message的另一個時機實際就是,當Message物件從佇列中取出處理完成之後,就會進行回收,放入池內。
如果具體閱讀MessageQuene的原始碼,我們會發現還有多種不同的回收時機,我們簡單總結幾種常見的時機:
- 人為呼叫message.recycle()來回收物件。
- message從佇列中取出被處理完成後會自動回收。
- 呼叫Lopper.quit()/quitSafely(),該方法最終會呼叫MessageQuene的quit()方法。
- MessageQuene呼叫quit在合適的時機將自身佇列中的Message物件進行回收。
- MessageQuene的quit方法還會將我們之前談到的mQuitting設定為true,這代表著當呼叫了quit之後,再通過handler來send任何message,都將被直接回收。
總結
到這裡,對於Android的訊息機制我們也就研究的差不多了。雖然我總覺得在寫的過程中,我本來還有個別想總結的點,但最後似乎忘了,也想不起了。
我們最後總結一個問題,那就是Handler為什麼能夠執行更新UI的操作!現在我們就來分析一下這個過程,回想一下:
通常我們在另開的執行緒中執行耗時操作,當耗時操作執行完畢後,則呼叫sendMessage()最終讓handler更新UI。
現在我們知道了,這時的Handler我們一定會是定義在主執行緒,即UI執行緒當中的。當我們在分執行緒中sendMessage的時候:
經過我們之前說到的一系列呼叫,最終會通過Handler來進行最終處理。而Handler本身是在主執行緒中執行的,自然也就能夠操作UI。
所以說,如果說Handler能夠讓我們更新UI,不如說其本質是將操作切換到該Handler所在的執行緒來執行。
我們可以試著傳送訊息給在分執行緒中建立的Handler物件,然後在handleMessage仍然試圖去訪問UI。會發現結果當然是行不通的。
這裡就說到我們之前說到的handler.post()這樣的使用方式了,因為引數型別是Runnable,所以我們很容易認為是通過handler執行一個執行緒任務。
但實際情況是,假設Handler物件是在主執行緒中建立的。那麼,通過post()方法,我們仍然可以去更新UI。這是為什麼?
這就回到了我們之前說的,當handler去dispatchMessage後,如果判斷msg.callback不等於null,就會通過msg.callback.run()來處理訊息。
這個時候實際本質上就是在切換到主執行緒去執行了Runnable物件的例項方法run()而已。所以當然能夠更新UI。
而我們可能會有這樣一種需求,那就是想要在指定的時間之後去執行一個耗時的執行緒任務,這個時候可能會想到Handler的postDelayed方法。
我想說的使用誤區就是,這個時候的Handler一定不要是建立在主執行緒當中的,因為這樣耗時操作最終還是在主執行緒執行,自然也就會引發ANR。
如果我們一定想要通過Handler實現我們這樣的需求,其實很簡單,當然就是要把Handler建立在分執行緒當中,就像下面這樣:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();;
final Handler handler = new Handler();
Looper.loop();
handler.post(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
handler.sendEmptyMessage(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}).start();