一.前言
本以爲這些東西是老生常談,並且作爲Android開發的基礎入門知識,是必須熟練掌握的,但是發現每每提起Handler及Looper,ThreadLocal的原理,總是要去翻閱資料,或者查看源碼再捋一遍,乾脆花一點時間整理一下,等下次再忘記拿出來稍稍翻閱一下就ok了。
二.原理淺析
分析一件事情往往需要帶着問題去分析,下面我們將從以下幾個問題進行分析:
- 爲什麼主線程直接new Handler就可以,子線程直接new Handler會報異常?報什麼異常?子線程創建handler應該需要什麼樣的步驟?
- 消息是怎麼進行跨線程傳遞的,最後是怎麼又傳回給了handler?
- Looper是怎麼做到每個線程都存在一個實例的?並且在不同的線程中取到的是對應線程的實例?ThreadLocal的原理是什麼樣的?
相信你如果能夠很詳盡的回答這三個問題,那麼本篇文章可能對你來說只是一個查漏補缺的過程,不過如果你對這三個問題回答模棱兩可甚至回答不出來,那麼請務必好好閱讀此篇文章。
首先我們先看下怎麼在主線程中創建Handler
private void initMainThreadHandler() {
mMainThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "mainThread \n" msg.toString());
}
};
}
如果同樣按照這種方式在子線程中創建Handler如下
private void initOtherThreadHandler() {
new Thread(new Runnable() {
@Override
public void run() {
mOtherThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "otherThread \n" msg.toString());
}
};
}
}).start();
}
那麼此時,調用initOtherThreadHandler
一定會報異常,報的異常爲:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
那爲什麼會報這個異常呢,我們可以追溯到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的賦值,是Handler的成員變量,後面會用到
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
可以看到Looper.myLooper()
得到的對象是null,所以就拋出了一個異常,那麼我們此時就要保證這個方法得到的對象不能爲空,至於爲什麼爲空呢?後面將ThreadLocal源碼會講到,優化後的子線程初始化handler如下:
private void initOtherThreadHandler() {
new Thread(new Runnable() {
@Override
public void run() {
//準備該線程對應的looper
Looper.prepare();
mOtherThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "otherThread \n" msg.toString());
}
};
//循環遍歷messageQueue來讀取消息隊列中的消息,從而能夠處理消息並保證線程在一直執行
Looper.loop();
}
}).start();
}
那有的同學可能會問了,爲什麼主線程不會拋異常? 那是因爲在應用App啓動的時候,會在執行程序的入口ActivityThread.class類中主函數public static void main(String[] args)裏面創建一個Looper對象:Looper.prepareMainLooper(),然後調用Looper.loop();完成Looper對象的創建。實際上Looper.prepareMainLooper()方法還是調用了Looper的prepare()方法完成Looper對象的創建。因此在主線程中通過關鍵字new創建的Handler對象之前,Looper對象已經存在並始終存在。
至此,我們解決了第一個問題,下面解決第二個問題。
上面我們創建了handler,那麼怎麼發消息呢:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.test1:
//給主線程發消息
Message obtain = Message.obtain();
obtain.obj = "主線程收到消息";
mMainThreadHandler.sendMessage(obtain);
break;
case R.id.test2:
//給子線程發消息
Message obtain2 = Message.obtain();
obtain2.obj = "子線程收到消息";
mOtherThreadHandler.sendMessage(obtain2);
break;
}
}
分別點擊兩個按鈕,打印日誌:
11-20 13:52:04.196 31131-31131/com.huli.hulitestapplication D/HandlerActivity: mainThread
{ when=-1ms what=0 obj=主線程收到消息 target=com.huli.hulitestapplication.activitys.HandlerActivity$1 }
11-20 13:52:14.166 31131-31163/com.huli.hulitestapplication D/HandlerActivity: otherThread
{ when=-1ms what=0 obj=子線程收到消息 target=com.huli.hulitestapplication.activitys.HandlerActivity$2$1 }
我們可以看到,給對應線程發消息,就是使用對應線程裏面創建的handler發消息,給主線程發消息,就是使用mMainThreadHandler
,給子線程發消息,就是使用mOtherThreadHandler
,那這個消息怎麼進到對應線程並處理,我們來看發消息的源碼:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
//這個mQueue是Handler的成員變量,是Looper的成員變量賦值過來的
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);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//這裏將消息的target屬性賦值爲handler,後續會用這個target來處理消息
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
//然後將消息塞進隊列中
return queue.enqueueMessage(msg, uptimeMillis);
}
可以看到,對應的發消息,其實就是將message的target指向當前發消息的handler,然後將message塞入消息隊列中,這個消息隊列是通過Looper.loop()
來進行遍歷的,我們繼續看源碼:
public static void loop() {
final Looper me = myLooper();
//這個隊列跟發消息的Handler裏面的隊列是一個對象,Handler裏面隊列添加了消息,這裏就能取到消息
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
try {
//關鍵代碼 使用調用msg.target.dispatchMessage方法處理message
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
}
}
由上面代碼,我們可以看到,最終就轉到了發消息的Handler來處理
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
//由於上面都爲空,所以走到這裏,調用下面的方法
handleMessage(msg);
}
}
//這個方法就是我們重寫的方法,最終打印出了日誌
public void handleMessage(Message msg) { }
所以我們瞭解到,消息是由handler進行發出,塞進對應線程的messageQueue中,對應線程的Looper又進行了遍歷,取出消息,交由對應發消息的handler來處理,進行了消息的發送和處理,那麼新的問題又來了,這個對應線程是怎麼來的?怎麼根據線程來獲得對應的Looper和messageQueue?這就是接下來我們要分析的第三個問題了。
我們會發現我們是通過Looper.prepare()
來進行對應線程Looper的初始化的,後面又通過Looper.myLooper()
進行對應線程的Looper的獲取的,這裏面肯定有貓膩,這就涉及到了ThreadLocal的原理了,接下來我們看源碼:
//Looper類
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
//從這也可以看出,每個線程Looper.prepare()只能調用一次,否則會拋異常
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
從上面就看到ThreadLocal
的身影了,我們先解釋下ThreadLocal:
由於內容比較多,這裏貼一篇詳細的文章,我下面做簡單的總結:
其實每個線程Thread
類裏面都有一個成員變量ThreadLocal.ThreadLocalMap threadLocals
,該map的key爲ThreadLocal
,value爲要創建的副本,當我們第一次調用Looper.prepare()
的時候,首先會調用sThreadLocal.get
方法,進到get
方法裏面得到當前線程的map,然後獲取key爲sThreadLocal
的value值,得到的爲null, 然後調用 sThreadLocal.set(new Looper(quitAllowed));
, 進到set
方法,同樣的獲取當前線程的map,然後給當前map設置key爲sThreadLocal
,value爲新創建的Looper,此時當前線程就存在Looper了
接下來我們創建Handler的時候,就會調用Looper.myLooer()
獲取當前線程的Looper,就是通過Looper的sThreadLocal.get
得到的
後面進行消息遍歷的時候,也是通過Looper.myLooer()
獲取當前線程的Looper,即通過Looper的sThreadLocal.get
得到的
所以這樣我們就回答了第3個問題,Looper能夠做到每個線程都有一個Looper副本,就是通過Looper的靜態成員變量sThreadLocal
做到的,它是通過獲取當前線程的ThreadLocalMap來判斷對應線程是否有副本,有就能取到當前線程的Looper副本,所以我們並沒有手動給Handler賦值Looper,它就能正常取到其值,就是通過ThreadLocal來做的。
如果在開發過程中你也有不同線程都存在一個副本的需求,就可以嘗試使用ThreadLocal來實現。
至此,大功告成。
本文由博客一文多發平臺 OpenWrite 發佈!