测试代码
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()
翻译:无法在没有调用 Looper.prepare() 的线程中创建 handler
报错信息有两点提示:
- Toast 需要创建 handler
- Handler 里需要有关联的Looper:调用 Looper.prepare
疑问:那么是不是为 Toast 内部的 Handler 关联一个 Looper 就可以成功弹 Toast 了呢?
我们平时在主线程创建 Handler 的时候,内部会获取当前线程关联的 Loper 对象(注意,是获取不是创建)。主线程的 Looper 对象是不需要我们手动创建的,是由应用启动时 Android 系统自动创建的。代码如下:
创建 Handler
public Handler(Callback callback, boolean async) {
...
//获取当前线程关联的Loper对象
mLooper = Looper.myLooper();
//如果当前线程没有关联的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 内获取保存的 Looper
return sThreadLocal.get();
}
//主线程 Looper set 的位置 --> ActivityThread.java
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
}
//创建主线程 Looper 对象 --> Looper.java
public static void prepareMainLooper() {
prepare(false);
}
//存储 Looper 对象到 ThreadLocal
private static void prepare(boolean quitAllowed) {
sThreadLocal.set(new Looper(quitAllowed));
}
以上是主线程的 Looper 对象的创建流程,由 Android 系统,具体是 ActivityThread.java 中的 mian 方法中调用Looper.prepareMainLooper();
可以看到,只要使用 Handler 对象,就必须为其创建一个关联的 Looper 对象,不然会抛出异常。而我们平时在主线程创建 Handler 的时候之所以不需要为其创建关联的 Looper 对象,是因为系统为我们做了这一步。而 Toast 在子线程使用时报错信息:
Can't create handler inside thread that has not called Looper.prepare()
翻译:无法在没有调用 Looper.prepare() 的线程中创建 handler
这个报错信息显然是因为没有给 Handler 关联其对应的 Looper 造成的。由此我们猜测,Toast 内部是使用到了 Handler 的。去查看下 Toast 源码:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
//参2为 Looper 对象,这里默认传入 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) {
...
//关键代码#####
mTN = new TN(context.getPackageName(), looper);
...
}
private static class TN extends ITransientNotification.Stub {
...
//内部 Handler
final Handler mHandler;
TN(String packageName, @Nullable Looper looper) {
//这里对指定的looper进行校验,
if (looper == null) {
// 使用 Looper.myLooper() 如果 looper 没有指定
looper = Looper.myLooper();
//#######案发现场#######
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
//#######案发现场#######
}
//这里创建了 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 的源码我们发现,Toast 的创建过程中,在 TN 这个类中确实创建了一个 Handler,并为其传入了 Looper 对象,这个 Looper 对象在构造 Toast 的过程中传入的一直是 null ,然后调用looper = Looper.myLooper();
从 ThreadLocal 中获取当前线程关联的 Looper 对象,而我们在子线程中是没有设置过 Looper 对象的,所以会抛出异常。所以这就是问题所在。要在子线程弹 Toast 就必须为其指定 Looper 。所以我们修改代码:
new Thread(()->{
Looper.prepare();
Toast.makeText(getApplicationContext(),"我弹",Toast.LENGTH_SHORT).show();
Looper.loop();
}).start();
成功~
总结:
子线程只是一个普通的线程,其 ThreadLoacl 中没有设置过 Looper,所以会抛出异常,要想子线程弹出 Toast ,需要为其制定 Looper 对象。
Toast 使用的无所谓是不是主线程 Handler,吐司操作的是 Window,不属于 checkThread 抛主线程不能更新 UI 异常的管理范畴。它用 Handler 只是为了用队列和时间控制排队显示吐司。
-
Toast 内部有两类 IPC 过程,
- 第一类是 Toast 访问 NotificationManagerService,
- 第二类是 NotificationManagerService 回调 Toast 里面的 TN 接口