相信很多安卓开发者都坚信一个信念,那就是子线程不能更新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()方法,代码如下:
@Subscribe(threadMode = ThreadMode.ASYNC)
public void receive(Friend friend){
// textView.setText(friend.toString() + "==" + Thread.currentThread().getName());
if (Looper.myLooper() == null) {
Looper.prepare();
}
Toast.makeText(this, friend.toString(), Toast.LENGTH_SHORT).show();
Looper.loop();
}
这里我们是在EventBus接受消息的方法里面,这个方法设置为Thread.ASYNC,即为子线程,在子线程中添加了Looper.prepare()以及Looper.loop()方法后,Toast的报错没有了,并且能够正常弹出toast。
结果证明在子线程里面是可以弹Toast。那么问题来了,显示Toast是UI操作是毋庸置疑的,那么就是我一直认为的子线程不能进行UI操作的认识有误区?
其实这里有个误解,Toast.show并不是所谓的更新UI操作!
Toast的显示是由调用线程的handler来处理的,即可以是非UI线程来操作,其布局的加载利用了WindowManagerImpl来实现了。
到这里已经愈发明显了,子线程显示Toast是没有问题的,但是Toast是一个比较特殊的UI,跟系统有关系。Toast本质上是一个window,跟activity是平级的,而我们平时所说的非UI线程不能更新UI,是因为在ViewRootImpl里面会有线程检查,checkThread只是Activity维护的View树的行为。
Toast使用的无所谓是不是主线程Handler,吐司操作的是window,不属于checkThread抛主线程不能更新UI异常的管理范畴。它用Handler只是为了用队列和时间控制排队显示吐司。
如果想要在子线程中显示Toast,那么我们要先自己创建looper,先Looper.prepare(),再show吐司,再Looper.loop()一样可以吐出来,只不过loop操作会阻塞这个线程,没人这么玩罢了。一般我们都是在主线程中showToast,都是让Toast用主线程的Handler,是因为主线程中自带有looper,这个是在ActivityThread里初始化的,本来就是阻塞处理所有的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)
上面说的当然是没有问题的,但是有些特殊情况是可以在子线程中进行更新UI的操作的,这个有点颠覆我们的认知了,有的人会说了我开发了这么多年都没有进行过这么骚的操作,还可以这样?其实,绝大多数情况下在子线程中确实是不可以更新UI的,但是满足一定的条件也是完全可以在子线程中更新UI的,例如下面的代码:
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable()
{
@Override
public void run() {
btn.setText("fei ui");
}
}).start();
}
大家试一试会发现,没有任何问题,握草还真在子线程中更新了UI,其实在OnCreate和OnResume之前都是可以的这么做的,等等,不是说好的不能在子线程中更新UI么?为啥这俩方法里面就可以了呢?首先我们要知道是谁在检查是否在子线程中更新UI并报错的,阅读源码,我们发现在ViewRootImpl中有个checkThread方法:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
了解整个activity创建以及启动流程后,会知道这个ViewRootImpl的创建就是在onResume之后,因此,我们在这个检查官创建出来之前进行子线程的更新UI是没有问题的,只是平时没有人这么做罢了。
除了上述在OnCreate以及OnResume中可以子线程更新UI外,在EventBus中创建的子线程也是可以更新UI的,如下:
@Subscribe(threadMode = ThreadMode.ASYNC)
public void receive(Friend friend){
textView.setText(friend.toString() + "==" + Thread.currentThread().getName());
// if (Looper.myLooper() == null) {
// Looper.prepare();
// }
// Toast.makeText(this, friend.toString(), Toast.LENGTH_SHORT).show();
// Looper.loop();
}
// @Subscribe
// public void receive(String s){
// textView.setText(s);
// }
这么写,其实就跟在XML中写是一样的,因为是在另外一个activity中发送的事件,在本activity中更新UI,这时还未走到本activity的生命周期,因此也是没有问题的。
大家应该已经明白子线程其实是可以操作UI的,只是必须使用适当的方法或者在适当的时机,比如用WindowManagerImpl可以在子线程中显示一个布局这个和传统意义说的activity上的View是不同的,或者在Activity中从onCreate直到onResume(包括onResume),都可以在子线程里面操作UI,只不过我们很少这样做罢了,而在onResume方法执行完之后,就不能在子线程里面操作该Activity的UI了。
ViewRootImpl、view、window绑定流程相关学习:
https://mp.csdn.net/editor/html/115266556
参考链接:https://blog.csdn.net/baidu_27196493/article/details/81662379