Android在子线程中操作UI:弹出Toast、改变TextView内容

郎鸿雪
2023-12-01

问题一:子线程能弹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()方法,代码如下:

@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和普通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结论:

如果想要在子线程中显示Toast,那么我们要先自己创建looper,先Looper.prepare(),再show吐司,再Looper.loop()一样可以吐出来,只不过loop操作会阻塞这个线程,没人这么玩罢了。一般我们都是在主线程中showToast,都是让Toast用主线程的Handler,是因为主线程中自带有looper,这个是在ActivityThread里初始化的,本来就是阻塞处理所有的UI交互逻辑。

 

问题二:除了上面说的TOAST,子线程可以更新activity的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的,例如下面的代码:

子线程更新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是没有问题的,只是平时没有人这么做罢了。

子线程更新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

 类似资料: