【Android 】为什么非 UI 线程不能更新 UI?

2,823 阅读2分钟

想深入讲清楚这个问题,需要多角度来回答

什么是UI线程

Android的核心进程zygote进程fork出我们的app,app启动的最终会走入到ActivityThread中的main方法,在main方法中会调用Looper。其中ActivityThread所在的线程被称为UI线程,也就是我们常说的主线程 (Main thread)。 关于Main thread这个称呼其实可以查看ActivityThread中main方法的源码:

 public static void main(String[] args) {
        ... ...
        //注释1
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注释1:只看最后两行代码,在Loop.loop();调用完直接抛出异常,这里异常提到了当前线程称之为Main thread.

UI线程的工作机制

需要理解UI线程的工作机制,就需要了解Android的消息机制。简单的图片能描述这个概念,如下图

UI线程为什么不能更新

构建一个子线程更新UI的例子

   protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        showTv = findViewById(R.id.show_tv);
        new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                showTv.setText("子线程显示UI");
            }
        }.start();
    }

运行之后会抛出异常

   android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7957)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1274)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at android.view.View.requestLayout(View.java:23120)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3172)
        at android.view.View.requestLayout(View.java:23120)
        at android.widget.TextView.checkForRelayout(TextView.java:8914)
        at android.widget.TextView.setText(TextView.java:5736)
        at android.widget.TextView.setText(TextView.java:5577)
        at android.widget.TextView.setText(TextView.java:5534)
        at com.example.myapplication.MainActivity$1.run(MainActivity.java:29)

这里在子线程中添加了Thread.sleep做一些耗时操作

从抛出的异常信息可以定位到相应的源码ViewRootImpl.java

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
           //注释2
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

注释2:从代码中可以看出抛出异常的一部分,会被requestLayout()调用,如果了解view的绘制过程可以知道requestLayout是被调用的方法之一。

在实例代码中添加了Thread.sleep()代码来执行耗时,如果没有这一耗时操作,执行setText更新UI的操作会被正常执行,个人看法:1. 这里需要再深入探究一下mHandlingLayoutInLayoutRequest这个boolean值变量的被改变的时机。2.在mThread初始赋值为Thread,执行到相关代码Thread.currentThread()没有变?多一点探究思路可以更好的阅读源码,有的放矢。

Android 是否提供了非UI线程更新UI的一些控件

这个问题,我们需要另外探究如SufaceView相关类的实现方式,就可以得到我们想要的答案。

非UI线程想要更新UI怎么办

Handler发送message的方式

postInvalidate()

这两种方式我们也可以间接的更新UI

思考

一个简单的问题,入手点其实还是有很多内容,这么多内容需要自己不断看源码发现总结来获取。