Android中非主线程更新UI一定会报错吗?

2021-08-06/2021-08-06

Android中非主线程更新UI一定会报错吗?

先给出结论,不一定,有以下代码为证

以下五种方法在子线程更新UI都不会报错

class ViewImplActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_impl)

//        thread {
//            textView.setOnClickListener {
//                textView.text = "${SystemClock.uptimeMillis()}"
//            }
//        }

//        thread {
//            Looper.prepare()
//            val button=Button(this)
//            windowManager.addView(button,WindowManager.LayoutParams())
//            button.setOnClickListener{
//                button.text="${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
//            }
//            Looper.loop()
//        }

//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            it.requestLayout()
//            thread {
//                textView.text="1234"
//            }
//        }

//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            textView.text="222"
//            thread {
//                textView.text="1234"
//            }
//        }

//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            thread {
//                textView.text="1234"
//            }
//        }


    }
}

xml文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".viewimpl.ViewImplActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

在解释为什么不会报错前,我们来看看如果会报错,报错的信息会是什么

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)

相信大家都对这段报错很熟悉,但为什么会报错呢?其实是由于ViewRootImpl的checkThread方法

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

可以看到这个会判断mThread是否等于当前线程,一般情况下,mThread是一个主线程对象,但是也有特殊情况,当mThread赋值不是主线程的对象不就避开了对线程的检查了吗?查看源码可以发现是在ViewRootImpl被创建的时候进行的赋值,那ViewRootImpl是什么时候被创建的呢?ViewRootImpl在android源码中只有一个地方被创建,那就是在WindowMangerGlobal这个类中,在WindowMangerGlobal的addView方法中

root = new ViewRootImpl(view.getContext(), display);

这个的view就是DecorView,view树中的根View

这样,这种方法在子线程中更新UI为什么不会报错就清楚了:

//        thread {
//            Looper.prepare()
//            val button=Button(this)
//            windowManager.addView(button,WindowManager.LayoutParams())
//            button.setOnClickListener{
//                button.text="${Thread.currentThread().name}:${SystemClock.uptimeMillis()}"
//            }
//            Looper.loop()
//        }

这种情况在子线程中调用windowManager的addView方法

那么mThread就被赋值为了子线程,此时在子线程中更新UI就没有问题了

再来分析这种情况

//        thread {
//            textView.setOnClickListener {
//                textView.text = "${SystemClock.uptimeMillis()}"
//            }
//        }

我们知道,是调用了ViewRootImpl的checkThread方法才导致子线程更新UI报错,那ViewRootImpl是什么呢?

不知道大家有没有想过,android一个activity的根View是DecorView,那DecorView的测量布局绘制是谁调用的呢?只有DecorView的这些过程被调用了,以DecorView为根View的的View才会触发测量布局绘制,其实DecorView测量布局绘制就是ViewRootImpl调用的

在ActivityThread的handleResumeActivity方法中(仅展示跟我想要讲的有关的代码,其余代码有兴趣的可以自己看)

ViewManager wm = a.getWindowManager();
ViewRootImpl impl = decor.getViewRootImpl();
wm.addView(decor, l);

就是在这里将DecorView与ViewRootImpl进行了绑定并在调用这个方法由ViewRootImpl调用doTraversal方法开始Decor的测量布局绘制

那么,ViewRootImpl是在Activity的onResume被调用后才与DecorView绑定,那么在onCreate方法中更新UI此时不会调用到ViewRootImpl的checkThread方法

但是如果让子线程休眠2s,viewRootImpl就和view关联了,就会报错

//        thread {
//             Thread.sleep(2000)
//            textView.setOnClickListener {
//                textView.text = "${SystemClock.uptimeMillis()}"
//            }
//        }
//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            it.requestLayout()
//            thread {
//                textView.text="1234"
//            }
//        }

//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            textView.text="222"
//            thread {
//                textView.text="1234"
//            }
//        }

这俩种情况原因是一样的

android为了避免多次重复测量,如果在控件被点击后主线程已经开始测量又在子线程中申请重绘但是又不需要(控件大小没有改变)就不会导致viewRootImpl的checkThread方法会调用

//        val textView = findViewById<TextView>(R.id.textView)
//        textView.setOnClickListener {
//            thread {
//                textView.text="1234"
//            }
//        }

这种情况和TextView有关,如果将textView的宽高改为固定大小而不是wrapContent就会报错


标题:Android中非主线程更新UI一定会报错吗?
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2021/08/06/1628246769101.html

评论
发表评论
       
       
取消