注册

Toast必须在UI(主)线程使用?

背景


依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。


非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。


直至前两天我的朋友 “林小海” 同学说toast不能在子线程中显示,这句话使我突然想起了点什么。


我觉得我有必要证明、并且纠正一下。


toast不能在子线程调用展示的结论真的是谬论~


疑点


前两天在说到这个toast的时候一瞬间对于只能在UI线程中调用展示的说法产生了两个疑点:




  1. 在子线程更新UI一般都会有以下报错提示:


    Only the original thread that created a view hierarchy can touch its views.


    但是,我们在子线程直接toast的话,报错的提示如下:


    Can't toast on a thread that has not called Looper.prepare()


    明显,两个报错信息是不一样的,从toast这条来看的话是指不能在一个没有调用Looper.prepare()的线程里面进行toast,字面意思上有说是不能在子线程更新UI吗?No,没有!这也就有了下面第2点的写法。




  2. 曾见过一种在子线程使用toast的用法如下(正是那时候没去深究这个问题):




        new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this.getApplicationContext(),"SHOW",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();

关于Looper这个东西,我想大家都很熟悉了,我就不多说looper这块了,下面主要分析一下为什么这样的写法就可以在子线程进行toast了呢?


并且Looper.loop()这个函数调用后是会阻塞轮循的,这种写法是会导致线程没有及时销毁,在toast完之后我特意给大家用如下代码展示一下这个线程的状态:


    Log.d("Wepon", "isAlive:"+t[0].isAlive());
Log.d("Wepon", "state:" + t[0].getState());

D/Wepon: isAlive:true
D/Wepon: state:RUNNABLE

可以看到,线程还活着,没有销毁掉。当然,这种代码里面如果想要退出looper的循环以达到线程可以正常销毁的话是可以使用looper.quit相关的函数的,但是这个调用quit的时机却是不好把握的。


下面将通过Toast相关的源码来分析一下为什么会出现上面的情况?


源码分析


Read the fuck source code.


1.分析Toast.makeText()方法


首先看我们的调用Toast.makeText,makeText这个函数的源码:


    // 这里一般是我们外部调用Toast.makeText(this, "xxxxx", Toast.LENGTH_SHORT)会进入的方法。
// 然后会调用下面的函数。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}

/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used. // 1. 注意这一句话
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {

// 2. 构造toast实例,有传入looper,此处looper为null
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

从上面的源码中看第1点注释,looper为null的时候会调用Looper.myLooper(),这个方法的作用是取我们线程里面的looper对象,这个调用是在Toast的构造函数里面发生的,看我们的Toast构造函数:


2.分析Toast构造函数


    /**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 1.此处创建一个TN的实例,传入looper,接下来主要分析一下这个TN类
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}

TN的构造函数如下,删除了一部分不重要代码:


TN(String packageName, @Nullable Looper looper) {
// .....
// ..... 省略部分源码,这
// .....

// 重点
// 2.判断looper == null,这里我们从上面传入的时候就是null,所以会进到里面去。
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
// 3.然后会调用Looper.myLooper这个函数,也就是会从ThreadLocal<Looper> sThreadLocal 去获取当前线程的looper。
// 如果ThreadLocal这个不太清楚的可以先去看看handler源码分析相关的内容了解一下。
looper = Looper.myLooper();
if (looper == null) {
// 4.这就是报错信息的根源点了!!
// 没有获取到当前线程的looper的话,就会抛出这个异常。
// 所以分析到这里,就可以明白为什么在子线程直接toast会抛出这个异常
// 而在子线程中创建了looper就不会抛出异常了。
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
// 5.这里不重点讲toast是如何展示出来的源码了,主要都在TN这个类里面,
// Toast与TN中间有涉及aidl跨进程的调用,这些可以看看源码。
// 大致就是:我们的show方法实际是会往这个looper里面放入message的,
// looper.loop()会阻塞、轮循,
// 当looper里面有Message的时候会将message取出来,
// 然后会通过handler的handleMessage来处理。
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// .... 省略代码
case SHOW: // 显示,与WindowManager有关,这部分源码不做说明了,可以自己看看,就在TN类里面。
case HIDE: // 隐藏
case CANCEL: // 取消
}
}
};
}

总结


从第1点可以看到会创建TN的实例,并传入looper,此时的looper还是null。


进入TN的构造函数可以看到会有looper是否为null的判断,并且在当looper为null时,会从当前线程去获取looper(第3点,Looper.myLooper()),如果还是获取不到,刚会抛出我们开头说的这个异常信息:Can't toast on a thread that has not called Looper.prepare()。


而有同学会误会只能在UI线程toast的原因是:UI(主)线程在刚创建的时候就有创建looper的实例了,在主线程toast的话,会通过Looper.myLooper()获取到对应的looper,所以不会抛出异常信息。


而不能直接在子线程程中toast的原因是:子线程中没有创建looper的话,去通过Looper.myLooper()获取到的为null,就会throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");


另外,两个点说明一下:



  1. Looper.prepare() 是创建一个looper并通过ThreadLocal跟当前线程关联上,也就是通过sThreadLocal.set(new Looper(quitAllowed));
  2. Looper.loop()是开启轮循,有消息就会处理,没有的话就会阻塞。

综上,“Toast必须在UI(主)线程使用”这个说法是不对滴!,以后千万不要再说toast只能在UI线程显示啦.....


作者:ywp0919
链接:https://juejin.cn/post/6844904103538065422
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册