依稀记得,从最开始干Android这一行就常常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展现。android
很是惭愧的是,我以前也这么认为,而且这个问题也一直没有深究。bash
直至前两天个人朋友 “林小海” 同窗说toast不能在子线程中显示,这句话使我忽然想起了点什么。markdown
我以为我有必要证实、而且纠正一下。ide
toast不能在子线程调用展现的结论真的是谬论~函数
前两天在说到这个toast的时候一瞬间对于只能在UI线程中调用展现的说法产生了两个疑点:oop
在子线程更新UI通常都会有如下报错提示:源码分析
Only the original thread that created a view hierarchy can touch its views.ui
可是,咱们在子线程直接toast的话,报错的提示以下:this
Can't toast on a thread that has not called Looper.prepare()spa
明显,两个报错信息是不同的,从toast这条来看的话是指不能在一个没有调用Looper.prepare()的线程里面进行toast,字面意思上有说是不能在子线程更新UI吗?No,没有!这也就有了下面第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.
首先看咱们的调用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构造函数:
/**
* 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()");
另外,两个点说明一下:
综上,“Toast必须在UI(主)线程使用”这个说法是不对滴!,之后千万不要再说toast只能在UI线程显示啦.....