子线程能弹Toast吗?
相信很多安卓开发者都坚信一个信念,那就是子线程不能更新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么》。
最后调侃一句,由此问题引发对人生的的思考:只要是人定规则的,都不是固定不能变的,都可能有漏洞可钻的,引用电视剧《大明王朝》中的一句台词“没有什么是固若金汤的”。