最新消息:欢迎访问Android开发中文站!商务联系QQ:1304524325

子线程能弹Toast吗?

新手入门 loading 813浏览 0评论

相信很多安卓开发者都坚信一个信念,那就是子线程不能更新UI,不能进行UI操作,写此文之前,我自己也是这么坚信的,直到我注意到一个异常,才引发我对子线程不能更新UI有了新的认识。这个异常是在我在子线程里面不小心弹了一个Toast引发的,该异常相信很多朋友都见过,就是

java.lang.RuntimeException: Can’t create handler inside thread that has not called Looper.prepare()

这个异常本身倒是没什么,我奇怪的就是为什么不是提示非UI线程不能更新UI这样的异常,如下面所示:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7534)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1200)
at android.view.View.requestLayout(View.java:19996)

既然报的是没有调用Looper.prepare()的异常,那么如果我新建一个子线程,然后调用了Looper.prepare(),是不是就能弹Toast了,就能操作UI了?我们用一小段代码测试一下,代码很简单,就是在一个Activity里面新建一个线程,在线程的run方法里面先调用Looper.prepare(),然后调用显示Toast的代码,最后别忘了调用Looper.loop()方法,代码如下:


@Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 new Thread(new Runnable()
 {

 @Override
 public void run() {
 if(Looper.myLooper() == null)
 {
 Looper.prepare();
 }
 Toast.makeText(ServiceTestActivity.this,"test",Toast.LENGTH_LONG).show();
 Looper.loop();
 }
 }).start();
 }

结果证明我的猜想是正确的,子线程里面是可以弹Toast。那么问题来了,显示Toast是UI操作是毋庸置疑的,那么就是我一直认为的子线程不能进行UI操作的认识有误区?答案其实有两种可能:一是Toast的显示可能还是是由主线程操作的,可能是由主线程的Handler来处理的;二是Toast的显示就是由子线程操作的,子线程不能进行UI操作的说法存在误区。

为此弄明白这个问题,我特意跟踪分析了Toast的整个显示流程,该流程见我的另一篇博客《安卓Toast显示流程分析》,从源码上看,Toast的显示是由调用线程的handler来处理的,即可以是非UI线程来操作,其布局的加载利用了WindowManagerImpl来实现了。

到这里已经愈发明显了,子线程显示Toast是没有问题的,但是Toast是一个比较特殊的UI,跟系统有关系,子线程能否操作Activity里面的UI呢,为此我又做了一个实验,就是在onResume里面新建一个线程用于操作一个Button控件,代码如下:


@Override
 protected void onResume() {
 super.onResume();
 new Thread(new Runnable()
 {

 @Override
 public void run() {
 btn.setText("fei ui");
 }
 }).start();
 btn.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 new Thread(new Runnable()
 {

 @Override
 public void run() {
 btn.setVisibility(View.GONE);
 }
 }).start();
 }
 });
 }

测试结果就是第一个子线程成功修改了按钮的文字,没有报任何异常,而在按钮的点击事件里面,新建一个线程去修改按钮,就会报CalledFromWrongThreadException:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7534)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1200)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.requestLayout(View.java:19996)
at android.view.View.setFlags(View.java:11572)
at android.view.View.setVisibility(View.java:8082)

好了,讲到这里,大家应该已经明白子线程其实是可以操作UI的,只是必须使用适当的方法或者在适当的时机,比如用WindowManagerImpl可以在子线程中显示一个布局,或者在Activity中从onCreate直到onResume(包括onResume),都可以在子线程里面操作UI,只不过我们很少这样做罢了,而在onResume方法执行完之后,就不能在子线程里面操作该Activity的UI了。

至于为什么会报CalledFromWrongThreadException,这跟一个叫ViewRootImpl的类有关,该异常就是从这个类里面报出来的。这个类有一个Thread类的属性mThread,该属性的值就是创建ViewRootImpl对象的线程,在执行某些方法的时候会检查当前线程和创建ViewRootImpl对象所在的线程是否为同一线程。


void checkThread() {
 if (mThread != Thread.currentThread()) {
 throw new CalledFromWrongThreadException(
 "Only the original thread that created a view hierarchy can touch its views.");
 }
 }

Activity中ViewRootImpl对象的创建都是在UI线程中,所以mThread指向的就是main线程对象,并且Activity的ViewRootImpl对象的创建是在执行完Activity的onResume方法之后,所以在onResume之前(包括onResume),都可以在子线程操作UI,因为此时ViewRootImpl对象还没有创建,在onResume方法之后,子线程操作UI就会报异常了。关于ViewRootImpl的问题本文只做简单讲解,若想进一步了解,推荐一篇博客《Android子线程真的不能更新UI么》。

最后调侃一句,由此问题引发对人生的的思考:只要是人定规则的,都不是固定不能变的,都可能有漏洞可钻的,引用电视剧《大明王朝》中的一句台词“没有什么是固若金汤的”。

转载请注明:Android开发中文站 » 子线程能弹Toast吗?

您必须 登录 才能发表评论!