測試代碼
new Thread(()->{
Toast.makeText(getApplicationContext(),"我彈",Toast.LENGTH_SHORT).show();
}).start();
報錯信息
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
翻譯:無法在沒有調(diào)用 Looper.prepare() 的線程中創(chuàng)建 handler
報錯信息有兩點提示:
- Toast 需要創(chuàng)建 handler
- Handler 里需要有關(guān)聯(lián)的Looper:調(diào)用 Looper.prepare
疑問:那么是不是為 Toast 內(nèi)部的 Handler 關(guān)聯(lián)一個 Looper 就可以成功彈 Toast 了呢?
我們平時在主線程創(chuàng)建 Handler 的時候,內(nèi)部會獲取當(dāng)前線程關(guān)聯(lián)的 Loper 對象(注意蓖捶,是獲取不是創(chuàng)建)余寥。主線程的 Looper 對象是不需要我們手動創(chuàng)建的莉掂,是由應(yīng)用啟動時 Android 系統(tǒng)自動創(chuàng)建的萎馅。代碼如下:
創(chuàng)建 Handler
public Handler(Callback callback, boolean async) {
...
//獲取當(dāng)前線程關(guān)聯(lián)的Loper對象
mLooper = Looper.myLooper();
//如果當(dāng)前線程沒有關(guān)聯(lián)的Looper,則拋出異常
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
...
}
//Looper 獲取方法 --> Looper.java
public static @Nullable Looper myLooper() {
//從 ThreadLocal 內(nèi)獲取保存的 Looper
return sThreadLocal.get();
}
//主線程 Looper set 的位置 --> ActivityThread.java
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
}
//創(chuàng)建主線程 Looper 對象 --> Looper.java
public static void prepareMainLooper() {
prepare(false);
}
//存儲 Looper 對象到 ThreadLocal
private static void prepare(boolean quitAllowed) {
sThreadLocal.set(new Looper(quitAllowed));
}
以上是主線程的 Looper 對象的創(chuàng)建流程软免,由 Android 系統(tǒng),具體是 ActivityThread.java 中的 mian 方法中調(diào)用Looper.prepareMainLooper();
可以看到训貌,只要使用 Handler 對象灌危,就必須為其創(chuàng)建一個關(guān)聯(lián)的 Looper 對象康二,不然會拋出異常。而我們平時在主線程創(chuàng)建 Handler 的時候之所以不需要為其創(chuàng)建關(guān)聯(lián)的 Looper 對象乍狐,是因為系統(tǒng)為我們做了這一步赠摇。而 Toast 在子線程使用時報錯信息:
Can't create handler inside thread that has not called Looper.prepare()
翻譯:無法在沒有調(diào)用 Looper.prepare() 的線程中創(chuàng)建 handler
這個報錯信息顯然是因為沒有給 Handler 關(guān)聯(lián)其對應(yīng)的 Looper 造成的。由此我們猜測浅蚪,Toast 內(nèi)部是使用到了 Handler 的藕帜。去查看下 Toast 源碼:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
//參2為 Looper 對象,這里默認(rèn)傳入 null
return makeText(context, null, text, duration);
}
//隱藏方法
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
...
return result;
}
//隱藏方法
public Toast(@NonNull Context context, @Nullable Looper looper) {
...
//關(guān)鍵代碼#####
mTN = new TN(context.getPackageName(), looper);
...
}
private static class TN extends ITransientNotification.Stub {
...
//內(nèi)部 Handler
final Handler mHandler;
TN(String packageName, @Nullable Looper looper) {
//這里對指定的looper進行校驗,
if (looper == null) {
// 使用 Looper.myLooper() 如果 looper 沒有指定
looper = Looper.myLooper();
//#######案發(fā)現(xiàn)場#######
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
//#######案發(fā)現(xiàn)場#######
}
//這里創(chuàng)建了 Handler 并傳入指定的 Looper
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
...
break;
}
case HIDE: {
...
break;
}
case CANCEL: {
...
break;
}
}
}
};
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
mHandler.obtainMessage(CANCEL).sendToTarget();
}
}
通過查看 Toast 的源碼我們發(fā)現(xiàn)惜傲,Toast 的創(chuàng)建過程中洽故,在 TN 這個類中確實創(chuàng)建了一個 Handler,并為其傳入了 Looper 對象盗誊,這個 Looper 對象在構(gòu)造 Toast 的過程中傳入的一直是 null 时甚,然后調(diào)用looper = Looper.myLooper();
從 ThreadLocal 中獲取當(dāng)前線程關(guān)聯(lián)的 Looper 對象,而我們在子線程中是沒有設(shè)置過 Looper 對象的哈踱,所以會拋出異常荒适。所以這就是問題所在。要在子線程彈 Toast 就必須為其指定 Looper 开镣。所以我們修改代碼:
new Thread(()->{
Looper.prepare();
Toast.makeText(getApplicationContext(),"我彈",Toast.LENGTH_SHORT).show();
Looper.loop();
}).start();
成功~
總結(jié):
子線程只是一個普通的線程刀诬,其 ThreadLoacl 中沒有設(shè)置過 Looper,所以會拋出異常邪财,要想子線程彈出 Toast 陕壹,需要為其制定 Looper 對象。
Toast 使用的無所謂是不是主線程 Handler树埠,吐司操作的是 Window糠馆,不屬于 checkThread 拋主線程不能更新 UI 異常的管理范疇。它用 Handler 只是為了用隊列和時間控制排隊顯示吐司怎憋。
-
Toast 內(nèi)部有兩類 IPC 過程又碌,
- 第一類是 Toast 訪問 NotificationManagerService,
- 第二類是 NotificationManagerService 回調(diào) Toast 里面的 TN 接口