子线程能弹Toast吗

子线程能弹Toast吗?

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

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

这个异常本身倒是没什么,我奇怪的就是为什么不是提示非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操作是毋庸置疑的,那么就是鄙人一直认为的子线程不能进行UI操作的认识是有误区的?答案其实有两种可能:一是Toast的显示可能还是是由主线程操作的,可能是由主线程的Handler来处理的;而是Toast的显示就是由子线程操作的,子线程不能进行UI操作的说法存在很大误区。

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

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

@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么》。

猜你喜欢

转载自blog.csdn.net/baidu_27196493/article/details/81662379