当ViewRootImpl#checkThread检查到线程不一致时会抛出异常

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

按照上图中的操作,主线程创建TextView,线程池更新UI,如果没有保护机制到了这里app已经crash了,神奇之处在于应用不但没有出现crash,而且文字也成功更新了,只是组件宽度没有发生变化

问题一:为什么没有crash

线程池的异常保护机制吞掉了异常,所以应用没有出现崩溃

问题二:为什么更新后的文字能被显示

在checkThread被调用之前,文字的赋值流程已经完成,checkThread抛出的异常中断了TextView的measure、layout、draw过程,当其他组件更新UI时(比如这里的button),触发了TextView的draw,让更新后的文字绘制到了界面上

排查验证的过程中发现了几点有意思的现象

  1. 线程池更新UI后,主线程无法再正常更新UI
  2. 任何线程都可以打开DialogFragment,主线程可以正常更新其UI
  3. HandlerThread能打开Dialog并且更新其UI

以下分析基于api 30(Android 11)

View#requestLayout

看下View#requestLayout的逻辑,方法体内会执行mPrivateFlags标志位更新,将View标记为需要重新layout,然后判断parent是否需要requestLayout(判断的就是mParent的mPrivateFlags标志位),一直向上追溯到ViewRootImpl

	// View#requestLayout
    public void requestLayout() {
        ...
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        ...
	# View#requestLayout
    public boolean isLayoutRequested() {
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

正常的流程中View#layout阶段会重置mPrivateFlags

	// View#layout
    public void layout(int l, int t, int r, int b) {
    	...
    	mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    	...

但是由于ViewRootImpl#checkThread中抛出了异常,中断了正常的绘制流程,导致mPrivateFlags标志位没有被重置,所以无法正常更新UI,后面无论什么线程触发UI更新,因为标志位没有被重置mParent.isLayoutRequested()始终为true,都不会执行mParent.requestLayout(),所以无法正常更新UI

对应上面时序图中第④步被中断

DialogFragment的启动

任何线程都可以打开DialogFragment,主线程可以正常更新其UI

DialogFragment启动会使用宿主(这里可以认为是activity)的handler去post消息,所以会切换到主线程

	// FragmentManagerImpl
    void scheduleCommit() {
        synchronized (this) {
            boolean postponeReady =
                    mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
            boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
            if (postponeReady || pendingReady) {
            	// 使用宿主的handler
                mHost.getHandler().removeCallbacks(mExecCommit);
                mHost.getHandler().post(mExecCommit);
                updateOnBackPressedCallbackEnabled();

子线程不能更新UI?

以TextView为例更新UI的过程如下:
View#requestLayout -> ViewRootImpl#requestLayout -> ViewRootImpl#checkThread
checkThread会检查创建ViewRootImpl的线程和当前执行requestLayout的线程是否为同一个,若不是则抛出异常,因为正常情况ViewRootImpl的创建都是在main线程,导致出现了一个认知假象–子线程不能更新UI

那好我就喜欢不走寻常路,我用子线程创建UI并更新,看效果如何,这里以Dialog为例(不是DialogFragment)?
很好,得到了一个异常,它告诉我们需要用一个有Looper的线程创建handler,因为Dialog中使用了Handler
在这里插入图片描述
那就使用有looper的线程进行创建试下呢,这里为了方便直接使用HandlerThread,发现dialog可以正常打开,可以更新UI,但此时使用主线程更新UI会出现崩溃
在这里插入图片描述
主线程更新时出现的crash,从报错信息可以看出是checkThread检查到了创建和更新的线程不一致
在这里插入图片描述
既然非主线程也可以更新UI的话,为什么不用子线程更新更新UI,来减轻主线程的压力呢?
网上都在说Android UI组件是非线程安全的,所以不能多线程更新,如果我只用一个子线程去维护一部分UI更新,不涉及多线程呢?
这里可能涉及到一个UI粒度的问题,ViewRootImpl是Window维度创建的,所以只能看起来不能直接使用子线程维护细粒度的UI更新,但是这里了解到一个特殊的组件SurfaceView,子线程更新UI是否有价值这个问题需要后续继续探究下

欢迎关注公众号“从技术到艺术”
本文实例讲述了Android编程开发之TextView文字显示和修改方法。分享给大家供大家参考,具体如下: 一. 新建一个Activity 和 Layout 首先在layout文件夹中新建一个activity_main.xml,在新建工程的时候一般默认会新建此xml文件,修改其代码如下: activity_main.xml 代码 <RelativeLayout xmlns:android=http://schemas.android.com/apk/res/android xmlns:tools=http://schemas.android.com/tools android
今天在添加功能的过程中遇到了一个ImageView及TextView在收到回调的内容后,通过handler.sendEmptyMessageDelayed(COMPLETED, 100);在handleMessage方法里做tv.settext(), iv.setImageBitmap操作,发现一个问题,如果刚进入页面就回调,就可以正常显示,但是如果进入页面延迟3-5秒再回调就无法显示,打印log都有内容,就是没显示出来。折腾了两个小时,通过以下方式解决,在此做个标记,有遇到同样问题的童鞋么?
上次写了Android上调用百度人脸识别接口 但是只写到获取JSON字符串,在log当中打印,没有更新UI界面 这次简单写一下Android线程当中更新UI界面(TextView、ImageView) 这次调用的本地服务器接口,将图片上传到服务器中,返回JSON解析后再展示 通过Message传递消息给Handler更新界面 MessgActivity package ss.demo16;
问题一:线程能弹Toast吗? 相信很多安卓开发者都坚信一个信念,那就是线程不能更新UI,不能进行UI操作,写此文之前,我自己也是这么坚信的,直到我注意到一个异常,才引发我对线程不能更新UI有了新的认识。这个异常是在我在线程里面不小心弹了一个Toast引发的,该异常相信很多朋友都见过,就是 java.lang.RuntimeException: Can't create handle...
最近遇到一问题,ListView Item加载多个图片,图片是在Adapter的getView方法通过线程异步进行加载的。 这时候就涉及到线程刷新主线程中View的问题,一般有两个方式, 1.View.post(Runnable); 2.Activity.runOnUiThread(Runnable); 首次是使用View.post方式来刷新界面,但是一直刷新失败,debug发现图片下...
//在类里声明一个Handler Handler mTimeHandler = new Handler() { public void handleMessage(Message msg) { if (msg.what == 0) { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setConten...
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views private TextView txtContent; @Override protected void onCreate(@Nullable Bundle savedInstanceState) {