圖文解析Android 訊息處理機制
之前也看過不少的書或部落格介紹Android 訊息處理機制的知識點,但總是剛看完感覺懂了,過幾天就忘,今天一邊看原始碼,一邊畫了類圖和時序圖,希望可以加深自己的理解。
Android應用啟動時會建立一個主執行緒,與Android UI工具包互動,因此也叫UI執行緒,因此不能被一些耗時長的操作阻塞,阻塞時通常的表現是UI控制元件的事件不能得到響應,比如按鈕按下去了彈不回來,長時間還會造成ANR,Android提供的訊息處理機制就是來解決這個問題的,其中涉及的主要的類是Handler、Looper和MessageQueue。其類圖如下,簡單起見只列出了一些主要的方法。
其中MessageQueue為訊息佇列,Looper負責建立並管理訊息佇列,使其進入一個迴圈,Handler負責傳送訊息以及處理訊息。傳送訊息一般在耗時操作執行完後在工作執行緒進行,處理訊息一般在主執行緒進行。以下為Android訊息處理機制的時序圖:
時序圖中1-4步是訊息佇列的建立過程。
5-11步為執行緒進入一個迴圈,並監聽是否有訊息要處理,如果沒有訊息,執行緒就會進入睡眠等待狀態。
12-16步為訊息傳送過程。
17-19步為訊息的處理過程。
以上就是android非同步訊息處理的整個過程。下面結合具體應用進一步分析。
下面是一段最簡單的應用程式碼。
package blogxuan.bloghandler; import android.app.Activity; import android.app.ProgressDialog; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { ProgressDialog progressDialog; Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 0) { progressDialog.cancel(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startBtn = (Button) findViewById(R.id.button); progressDialog = new ProgressDialog(MainActivity.this); startBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { progressDialog.show(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } handler.sendEmptyMessage(0); } }).start(); } }); } }
點選按鈕後主介面會顯示一個進度對話方塊,然後會新建一個執行緒,Thread的sleep方法模擬一個耗時操作,執行完後handler傳送訊息,然後在主執行緒中handler的handleMessage方法收到訊息後關閉進度對話方塊。程式碼中可以看出只有流程圖中的訊息傳送過程以及訊息處理過程,並沒看到1-11步訊息佇列的建立過程以及迴圈過程。這是因為UI執行緒預設會給我們進行這兩個過程。在Acitvity的原始碼中可以找到一個叫mMainThread的ActivityThread類變數,而Looper和MessageQueue的建立就是在ActivityThread這個類的main方法中進行的,如下所示:
public static void main(String[] args) {
SamplingProfilerIntegration.start();
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
if (sMainThreadHandler == null) {
sMainThreadHandler = new Handler();
}
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
雖然沒有時序圖中的Looper.prepare()方法,但其實Looper.prepareMainLooper()這個方法也會間接呼叫prepare()方法。一旦呼叫prepare()方法,便會完成Looper物件以及MessageQueue物件的建立,同時完成C++層的NativeMessageQueue和Looper的繫結,不在深入。
呼叫Looper.prepare()只是建立訊息佇列的過程,呼叫Looper.loop()方法才能使訊息佇列進入一個迴圈過程並對訊息進行監聽。
UI執行緒建立的時候自動建立了訊息佇列並進入迴圈過程,這樣我們就可以在完成工作執行緒的耗時操作後往UI執行緒的訊息佇列發訊息提示更新UI元件。
但是如果我們希望一個相反的過程,也就是工作執行緒需要等待UI執行緒完成一些操作後再做一些操作,那麼我們必須自己在工作執行緒中實現Looper和MessageQueue的建立以及迴圈。
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
以上是Looper原始碼中的參考用例。這種用法自己暫時只用到過一次,當時專案中有個需求是實現非同步資料庫事務。當時開了一個子執行緒來呼叫了資料庫的beginTransaction()方法,當又開了另一個執行緒進行資料庫操作的時候發現報資料庫被鎖了,後來查資料發現一個完整的資料庫事務必須放在一個執行緒執行才行,但是根據專案的需求執行資料庫的操作是在UI執行緒發起的,因此就只能在開啟事務的子執行緒中實現Looper和MessageQueue(執行緒預設是沒有Looper和MessageQueue的),這樣就實現了子執行緒也能進行訊息迴圈監聽,當UI執行緒需要執行資料庫操作時再向子執行緒傳送訊息通知在子執行緒中繼續進行資料庫操作,就能保證資料庫的操作跟開啟事務是同一個子執行緒。
Looper物件的建立還有一個概念是ThreadLocal,Looper.prepare()方法完成了Looper物件的建立,其原始碼如下:
public static void prepare() {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper());
}
從程式碼中看到Looper物件建立並不像我們通常的方式new一個靜態全域性變數,或者是通過單例模式的方式建立,而是通過ThreadLocal物件來把這個Looper物件包含起來,ThreadLocal的原始碼的註釋說明如下:
/**
* Implements a thread-local storage, that is, a variable for which each thread
* has its own value. All threads share the same {@code ThreadLocal} object,
* but each sees a different value when accessing it, and changes made by one
* thread do not affect the other threads. The implementation supports
* {@code null} values.
*
* @see java.lang.Thread
* @author Bob Lee
*/
簡單來講就是可以實現同一個ThreadLocal物件,在不同的執行緒中有不同的值,並且不同執行緒之間的值互不干擾,為什麼需要這個東西呢,假設有兩個執行緒都需要實現looper機制,這兩個執行緒的Looper和MessageQueue肯定是不一樣的,而且一個執行緒只能有一個Looper和MessageQueue,這點從上面Looper.prepare()的原始碼看出,當在兩個執行緒各建立了一個Handler時,怎麼保證這兩個handler發的訊息不會發到別的執行緒的訊息佇列中呢?這就需要handler跟各自執行緒的Looper和MessageQueue繫結起來,而怎麼繫結呢? 下面是Handler建立的建構函式的程式碼:
public Handler() {
...
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 = null;
}
從程式碼可以看出是通過Looper.myLooper();方法來進行關聯的,Looper.myLooper()的程式碼如下:
public static Looper myLooper() {
return sThreadLocal.get();
}
簡單的一行程式碼就實現了繫結,而如果我們不用ThreadLocal,那麼就必須提供一個全域性的雜湊表給Handler來查詢關聯的執行緒的Looper了,這就是ThreadLocal的好處,至於ThreadLocal為什麼能實現這種效果,下面是ThreadLocal的set方法程式碼:
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
從這個方法大致可以看出set進去的value值(Looper)和Thread物件以及this(ThreadLocal)物件關聯了,而不同執行緒的values是不一樣的,所以當呼叫get方法取出Looper時就可以根據不同的執行緒物件取出不同的值了,就不往下分析了。
以上是Looper和MessageQueue的建立以及迴圈過程,下面分析下訊息的傳送過程和處理過程。
訊息的傳送過程通常是用handler的sendMessage方法,通過上面handler的類圖可以發現還有好多類似的方法,但最終都會走導sendMessageAtTime方法,因此時序圖中寫的是這個方法,原始碼如下:
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
{
boolean sent = false;
MessageQueue queue = mQueue;
if (queue != null) {
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
}
else {
...
}
return sent;
}
傳送訊息過程其實就是訊息入隊過程,之後便會喚醒阻塞的queue.next()方法。這個方法是在Looper.loop()方法中呼叫的,程式碼如下:
public static void loop() {
Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
MessageQueue queue = me.mQueue;
...
while (true) {
Message msg = queue.next(); // might block
if (msg != null) {
if (msg.target == null) {
// No target is a magic identifier for the quit message.
return;
}
...
msg.target.dispatchMessage(msg);
...
}
}
}
喚醒queue.next()後就會進入訊息處理過程,也就是msg.target.dispatchMessage(msg),而msg.target其實就是handler物件,從sendMessageAtTime的原始碼中就可以看出來,handler.dispatchMessage的程式碼如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
第十行的程式碼就會呼叫到在new Handler時重寫的方法。這裡還有必要說下第三行的handleCallback(msg)方法:
在公司的專案中經常看到一個這樣的用法:
handler.post(new Runnable() {
@Override
public void run() {
progressDialog.cancel();
}
});
handler在建立的時候並沒重寫handleMessage方法,為什麼可以這樣在run方法中進行ui操作呢?檢視handler的post原始碼:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
而sendMessageDelayed方法其實最終也會呼叫到上面講過的sendMessageAtTime方法,getPostMessage的程式碼如下:
private final Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
可以看到getPostMessage構造了一個訊息物件,物件的callback物件就是post方法中進行UI操作的的Runable物件,從上面dispatchMessage中原始碼的第三行檢視handleCallback程式碼如下:
private final void handleCallback(Message message) {
message.callback.run();
}
正是在這裡執行了run方法的操作,因為直接呼叫Runable的run方法並不會建立一個執行緒,因此如果handler關聯的是UI執行緒,在handler.post方法中進行UI操作是沒問題的,同理不能在這裡執行耗時操作。
以上就是自己對於Android訊息處理機制的理解,第一次認真寫一篇部落格,花費了快一天的時間,希望能堅持下去。