Handler執行機制中必須明白的幾個問題
概述
我在看完Handler的原始碼後有兩個感覺,一是貌似明白了很多東西,二是當問到具體問題時感覺還是模模糊糊。下面我們就帶著問題再看一次原始碼,力爭把這塊知識點搞的明明白白。
問題有:
- 在UI執行緒中有幾個Looper物件?有幾個MessageQueue物件?有幾個Handler物件?有幾個Message物件?
- 怎麼保證只有一個Looper物件的?
- 怎麼保證只有一個MessageQueue物件的?
- 為什麼傳送訊息在子執行緒,而處理訊息就變成主執行緒了,在哪兒跳轉的?
- looper物件只有一個,在分發訊息時怎麼區分不同的handler?
- 能不能在子執行緒中建立Handler物件?
- 怎麼在子執行緒中得到主執行緒中handler物件?
如果上面七個問題你都能清晰的回答,那麼恭喜你,你對Handler的理解已經很透徹了。如果你有時間,還是希望你能繼續往下看,可以給我找錯誤,找到我理解偏差的地方,找到有獎哦。或者您也可以提出問題讓我回答,大家共同學習。
在UI執行緒中有幾個Looper物件?有幾個MessageQueue物件?有幾個Handler物件?有幾個Message物件?
在UI執行緒中只有一個Looper物件,只有一個MessageQueue物件。但可以有很多個handler物件。可以有很多個Message物件。
怎麼保證只有一個Looper物件的?
在Handler機制中使用Looper物件的地方有三個,一是在ActivityThread類中使用Looper.prepareMainLooper()建立Looper物件,二是在hanlder類中使用Looper物件,三是在Looper的loop方法中使用Looper物件處理訊息。若這三個Looper物件是同一個就證明了在UI執行緒只有一個Looper物件。
一,Looper.prepareMainLooper()建立Looper物件
prepareMainLooper方法的原碼是:
public static void prepareMainLooper() {
prepare(false);//建立Looper物件,並儲存到ThreadLocal中
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared." );
}
sMainLooper = myLooper();//得到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));
}
分析:在第一次呼叫sThreadLocal.get()方法得到的一定是null,所以此時的重點是建立Looper物件,並放入sThreadLocal中儲存起來。
這兒要明確兩點:
1. sThreadLocal是Looper類的靜態欄位,所以只有一個sThreadLocal物件。
2. prepare方法在UI執行緒被呼叫,所以只有在Ui執行緒才能從sThreadLocal物件中獲取到looper物件。
下面看myLooper方法的原碼:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
分析:這個方法很簡單,目的就是得到UI執行緒中的Looper物件。注意這個方法是靜態方法,得到的Looper物件就是在UI執行緒中建立的Looper物件。
二,在Hanlder類中使用Looper物件
我們知道在建立Handler物件時使用的是無參構造,但無參構造方法呼叫了下面的構造方法:
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;
}
分析:由原碼可知,Handler類中有mLooper欄位和mQueue欄位。mLooper欄位賦值時通過Looper.myLooper()方法,我們從上面知道這個方法返回的值就是UI執行緒中建立的Looper物件,所以此時的Looper物件和Ui執行緒中建立的Looper物件是同一個。
三,在Looper的loop方法中使用Looper物件
在Looper類的loop方法中獲取Looper物件的程式碼是:
final Looper me = myLooper();
很明顯通過myLooper方法得到的Looper物件就是UI執行緒中建立的Looper物件。
怎麼保證只有一個MessageQueue物件的?
首先我們找一下MessageQueue物件是在哪兒建立的?
我們知道在在Looper的prepare方法中建立了Looper物件,並放入到ThreadLocal中,具體程式碼是:
sThreadLocal.set(new Looper(quitAllowed));
那麼我們來看一下Looper的構造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper的構造方法中建立了MessageQueue物件,並賦值給mQueue欄位。因為Looper物件只有一個,那麼Messagequeue物件肯定只有一個。
下面我們再多學習一點,找到使用MessageQueue使用的地方。MessageQueue被兩個地方使用,一是在handler的sendMessage中傳送訊息。二是在looper類的loop方法中取出訊息。
- 在看Handler的構造方法中我們知道,在構造方法中通過Looper.myPrepare方法得到mLooper物件,又通過mLooper.mQueue得到Messagequeue物件。所以此時這個MessageQueue物件就是在Looper的構造方法中建立的物件。
- 在Looper的loop方法中也是首先通過Looper.myPrepare()方法得到Looper物件,然後得到MessageQueue物件。
為什麼傳送訊息在子執行緒,而處理訊息就變成主執行緒了,在哪兒跳轉的?
傳送訊息使用的是Handler的sendMessage方法,這個方法最終呼叫的是enqueueMessage方法,enqueueMessage方法的原碼是:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
這個方法中呼叫的是MessageQueue的enqueueMessage方法,只要queue物件是UI執行緒中的MessageQueue物件,那麼就能被Ui執行緒中的Looper從訊息佇列中取出來,然後就在主執行緒執行了。我們知道Handler物件中的mQueue就是UI執行緒中的訊息佇列物件,所以在處理訊息時就是主執行緒了。
looper物件只有一個,在分發訊息時怎麼區分不同的handler?
關於這個問題主要看三點:
一是Message類中有個欄位target,這個欄位是Handler型別的。
二在Handler的enqueueMessage方法中有這麼一句程式碼:msg.target = this;即把handler物件與Message繫結在了一起。
三在Looper類的looper方法中分發訊息的程式碼是:msg.target.dispatchMessage(msg);
此時我們就明白了:在傳送訊息時handler物件與Message物件繫結在了一起。在分發訊息時首先取出Message物件,然後就可以得到與它繫結在一起的Handler物件了。
能不能在子執行緒中建立Handler物件?
這個問題並不是簡單的能不能的問題,考察的是對Handler的深入理解。答案肯定是可以的,但是僅僅使用無參構造是不可以的,還需要做其他的操作。
一,僅僅使用無參構造為什麼不能建立Handler物件?
我們知道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()");
}
}
分析:在上面程式碼中我們看到,如果得到的Looper物件是null就會丟擲異常。為什麼在子執行緒中Looper.myLooper方法會返回null呢?原因就是ThreadLocal的特性了。Looper.myLooper方法的原碼是從ThreadLocal中得到Looper物件,而在Looper.prepare方法中Looper物件是在UI執行緒中放入到ThreadLocal中的,所以在子執行緒中是得不到Looper物件的。在handler中沒有looper物件就會丟擲異常。
為什麼在handler中沒有looper物件就會丟擲異常?
我們知道handler傳送訊息最終呼叫的是MessageQueue的enqueueMessage方法,如果沒有Looper物件,肯定得不到MessageQueue物件,在傳送訊息時一定會丟擲nullPointException。所以系統在前面就進行了攔截,只要沒有Looper物件就不讓程式碼繼續執行。
怎麼在子執行緒中建立Handler物件?
在UI執行緒之所以可以直接建立建立Handler物件,是因為在Ui執行緒已經有了Looper物件,所以只要我們在子執行緒中建立Looper物件後就可以建立handler物件了。使用示例如下:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();//建立looper物件
Looper.loop();//開啟子執行緒中的訊息迴圈
Handler handler = new Handler(){//建立Handler物件
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}
}).start();
此時就在子執行緒中維護了一套訊息迴圈系統。
注意:這套訊息迴圈系統與UI執行緒中的Handler機制沒有任何關係,這兒的訊息不能與Ui執行緒中的訊息佇列進行通訊。但子執行緒中的訊息迴圈系統也有很大的作用,當需要處理很多訊息時可以讓訊息按序列執行。在圖片載入框架Picasso中就在子執行緒中維護了一套訊息迴圈系統,感興趣的小夥伴可以自行檢視Picasso的原碼。
怎麼在子執行緒中得到主執行緒中handler物件?
其實handler物件沒有主執行緒和子執行緒之分,有區分的是Looper物件,如果Looper物件是主執行緒中的,那麼handler就是主執行緒中的。
Handler有下面一個構造方法:
public Handler(Looper looper) {
this(looper, null, false);
}
這個構造方法中接收一個Looper物件。只要我們能到子執行緒中得到主執行緒的Looper物件,那麼就可以實現在子執行緒中得到主執行緒中的handler物件了。
在Looper類中有getMainLooper方法,這個方法的原始碼是:
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
這個方法返回的是在主執行緒中建立過的Looper物件。所以這個方法無論在哪兒呼叫得到的都是主執行緒中的Looper物件,此時我們就可以在子執行緒中建立主執行緒的handler物件了,程式碼如下:
new Thread(new Runnable() {
@Override
public void run() {
Handler handler = new Handler(Looper.getMainLooper());//得到UI執行緒中的handler物件
handler.post(new Runnable() {
@Override
public void run() {
//這兒寫邏輯程式碼
}
});
}
}).start();
注:使用這種方法可以輕鬆的從子執行緒跳轉到UI執行緒,完全不依賴於Activity或Application。圖片載入框架Picasso,網路請求控制元件Volley都是通過這種方法在工具類內部實現了更新UI。