Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

Android开发中我们会经常用到Toast来在界面上打印提示信息,如果是在调试程序的时候,使用它打印一些中间过程的结果显然比使用Log更直观一些,因为我们运行程序时可以直接从设备中看到弹出来的结果,而Log打印的内容还要通过控制台去找。但是估计也有一些人遇到过一些问题,比如Toast没有弹出来,好像没有起作用,或者直接会报异常。一般大家应该也知道,很多情况下Toast主要还是在主线程中去使用,直接在子线程中去使用的时候会报异常:Can't toast on a thread that has not called Looper.prepare()。但是如果必须要在子线程中去使用,该怎么做?最开始我是通过下面图1.的方式去在子线程中使用Toast:

图1.

Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

(如果使用这种方式,上面的代码逻辑当然可以自行优化,这里只举个简单例子)

上面这种方式也就是封装一下原生的Toast,通过Handler将Toast的调用post到主线程去,注意:这里创建Handler实例的时候一定要传入上述图中的参数--Looper.getMainLooper()才行。可以验证,这种方式是可以直接在线程中弹出Toast的,其实这和我们平时在子线程中通过handler将更新UI的操作发送到主线程是一个意思。

后来,我在网上又看到下面图2的封装方式:

图2.

Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

然后你在子线程中去调用这个封装好的Toast,可以发现也是可以正常弹出的。而且,从代码上看,似乎这种方式比图1中的高级一些:首先,前面说了,Toast在子线程中直接使用时有个异常:Can't toast on a thread that has not called Looper.prepare(),这里就针对这个问题自己调用了prepare还有loop两个方法,然后在这两个方法中间调用Toast的相关方法就OK了,平时我们可很少直接使用这两个方法吧?是不是感觉技术含量高一点?其次,因为前面说了图1其实就是将显示Toast内容的逻辑发送到主线程的消息队列中的,而图2这确实就是在当前子线程中执行,这种方式似乎还可以给主线程减少负担,毕竟主线程要处理的事太多,动不动就给人家找点事,指望着这点事也得让主线程自己去做,不利于发挥多线程的优势。讲真,我是感觉就图1这点事对主线程的影响真的可以忽略不计了????[笑哭.png]

但是,你可能很快就会发现问题,调用这个封装的Toast能在子线程中弹出来是没问题的,但是调用这个封装的Toast之后的逻辑似乎没有执行?没错,就是没有执行。为什么?再去看一下这个封装好的Toast方法代码,是不是有Looper.loop()的调用?问题想必说到这也清楚了:loop方法内部是一个for(;;)的无限循环,调用了这个方法之后,循环无法结束,它后边的代码当然得不到执行。然后你可能会问:上面不是也调用了quit方法?答案是自己去看一下这个quit的源码吧,这里不贴出来了。主要的意思就是这个quit方法里面就是清空了消息队列,没有结束这个无限循环,并不是你想的那样退出了这个loop......

总之,在子线程中通过自己调用Looper.prepare以及Looper.loop去使用Toast的话,本身可以,并且其他的主线程中的操作也可以放在这里(我没去验证),但是如果上面那样,这个loop之后还有其他的逻辑,那么这后面的就不能执行了。所以还是建议用图1中的方式去封装Toast,然后再去子线程中调用,如果不是子线程就不必进行这样的封装了。是否还有更好的其他方式,以及图2是否可以改动以后就行了?等着你给我答案,目前我没时间去探索[坏笑.jpg]

下面再说Toast可能没有弹出,而且"没有"(注意这里的引号)异常出现的情况:

你直接在某个地方使用原生的方式---Toast.makeText(context, content, Toast.LENGTH_LONG).show(),发现并没有Toast弹出,也没有像有的时候那样直接程序崩溃退出,这是怎么回事?这种情况基本可以说是你在子线程中直接使用了Toast,并且这个使用是在try...catch块中。因为本身直接在子线程中使用Toast就会像上面说的那样报异常,但是你如果这个Toast在try...catch块中,那么这个异常就被捕获了,也就没有崩溃,而直接到了catch中,所以Toast也没能弹出...就像下边这样的逻辑

图3.

Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

如果你能看一下控制台的log,细心的话就能看到下图中的这样:

图4。

Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

也就是代码逻辑从try中遇到Toast.make...开始后,异常就被catch捕获了,Toast根本没正常执行。

其实,最难的是你可能之前并不知道你的这个Toast是在子线程中使用并且被try...catch包围,因为你可能看到的是在Toast附近并没有try...catch包围它,也没有开子线程,但是这只是它的周围没有,如果是包含这个Toast的方法是在子线程中调用,而且是包含在try...catch中呢?是不是间接的满足了上面的条件,一样的意思?或者更外层的调用是在子线程中并且被包含在try...catch中呢?不能只看局部这一点吧?比如下图5的简单例子:test1方法中直接使用Toast,紧接着test1被test2调用,test3中又调用了test2,并且加了try...catch,直到最后第4步这里开了子线程去调用test3...此时上边说的两种条件都具备了,结果最终就是在控制台logcat那里看到了类似于图4的打印。所以,如果只看Toast所在的直接代码块显然是分析不出来问题的。

图5.

Android Toast:除了主线程之外,子线程中慎用,你可能遇到过Toast不弹出(不管用)的问题(和Looper搭配的方式也要慎重使用)

怎么去确定Toast当前所在的线程是不是子线程?用Log打印一下这个Toast当前所在的线程id不就知道是不是在子线程中了吗

总结:如果你对你当前要写Toast的地方的代码不熟,不确定你要写Toast的地方是否位于子线程中并且被try...catch包围,那么一定要用Toast的话,就用图1的封装方式吧。

最后,希望这篇文章里的一些内容没有误导人,还有就是希望有更好的解决问题方式分享出来。