测试代码
new Thread(()->{
Toast.makeText(getApplicationContext(),"我弹",Toast.LENGTH_SHORT).show();
}).start();
Screenshot_1558677502.png
报错信息
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();
成功~
Screenshot_1558677336.png
总结:
子线程只是一个普通的线程,其 ThreadLoacl 中没有设置过 Looper,所以会抛出异常,要想子线程弹出 Toast ,需要为其制定 Looper 对象。
Toast 使用的无所谓是不是主线程 Handler,吐司操作的是 Window,不属于 checkThread 抛主线程不能更新 UI 异常的管理范畴。它用 Handler 只是为了用队列和时间控制排队显示吐司。
Toast 内部有两类 IPC 过程,
第一类是 Toast 访问 NotificationManagerService,
第二类是 NotificationManagerService 回调 Toast 里面的 TN 接口