009 采坑-子线程更新UI-2

作者: 凤邪摩羯 | 来源:发表于2021-09-17 09:19 被阅读0次

先看一下截图吧

image

onResume方法是自定义的,在系统onResume方法中调用,但是依然没有闪退。
这个时候我的脑子也是一篇懵逼的。如果是onCreate开了子线程,然后子线程立刻更新UI,那是不会出现闪退的。但是沉睡5秒钟还是能修改成功,这就让我有点吃惊了。


所以我打算自己写一个demo试试看

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(5000);
                mTvTest.setText("子线程修改UI");
            }
        }).start();
    }

image

实际测试下来好像还是会闪退,这种情况才是我认为的现象。于是我把我的实验在群里发了一遍


我:我试了一下,子线程修改UI是会闪退的,你是怎么做到的
XXX:我再试试。
过了一段时间
XXX:奇怪了,我现在好像也试不出来了。。。
又过了一段时间
XXX:我用的是radioGroup+radioButton,然后修改的是radioButton的文案,可以在子线程里执行,weight设置为1,width设置为0。


上面这段对话让我更疑惑了。没有想到原因自然是写代码实验一下:

<?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=".MainActivity">
    <RadioGroup
        android:id="@+id/rg_group"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent">
        <RadioButton
            android:id="@+id/rb_test1"
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="这是第一个radiobutton"/>
        <RadioButton
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="这是第二个radiobutton"/>
    </RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

布局文件如上写完,然后写java代码:

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(5000);
                mRbTest1.setText("子线程修改UI");
            }
        }).start();
    }

run一下看下效果

image

竟然真的修改成功了!
这下就比较懵逼了,radioButton可以修改成功,难道radioButton做了什么特殊的处理么?随手去翻了一下radioButton的源码以及父类CompoundButton的源码,发现并没有特别之处。既然还是没找到原因,那么就debug源码看下具体的原因。
前面的流程一切正常,然后执行到checkForRelayout的时候就有问题了:

image

在checkForRelayout的方法里面,radioButton最终执行了invalidate方法直接return掉了。根据这篇文章可知我们抛出Only the original thread that created a view hierarchy can touch its views.这个异常是在checkThread方法里面,而checkThread是由于调用了requestLayout方法,这里没有执行requestLayout方法,自然不会崩溃。

  • 那么TextView是在什么地方执行的requestLayout呢?
  • 又是什么原因导致没有执行requestLayout方法呢?
    我们先来看第一个问题:其实只要截图中的两个条件都没有进入就会执行requestLayout方法
    第二个问题:回答这个问题首先看下checkForRelayout的完整代码:
    /**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    @UnsupportedAppUsage
    private void checkForRelayout() {

        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            ...代码省略...
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

首先看下最外层的判断条件,条件如果满足的时候就不会执行requestLayout,那么什么时候满足条件呢,需要具备以下几个条件

  1. 宽度不是wrap_content的或者mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth
  2. mHint == null || mHintLayout != null
  3. mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)
    其实这三个条件同时满足时就可以证明当前的View宽度是固定的并且宽度值是大于0的。然后我们再看下条件里面的代码:
           int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();

要想不执行requestLayout方法,那么我们首先必须满足(mEllipsize != TextUtils.TruncateAt.MARQUEE)条件表明当前TextView并不是走马灯的形式。然后进入接下来的条件

                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

这个条件要求我们如果高度是固定值的话那么就不会执行requestLayout方法了。那么如果高度不是固定值怎么办呢?接下来看下面的逻辑

                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }

当前View的高度等于修改UI之前的高度并且HintLayout等于空或者是HintLayout的高度也等于修改UI之前的高度,那么就不会执行requestLayout。什么意思呢?就是说即便高度是不固定的,但是只要修改前后高度一致,那么一样不会调用requestLayout。


这么看来只要View的宽度和高度在修改前后保持不变那么应该就不会去做requestLayout的,也就是说跟RadioButton没有什么关系,只是恰好这么设置以后radioButton的宽高是固定的,那么再来看下高度不固定但是修改前后保持一致是否也是可以修改成功的:

<?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=".MainActivity">
    <TextView
        android:id="@+id/tv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>


    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(5000);
                mTvTest.setText("子线程修改UI");
            }
        }).start();

看下这样的运行结果

image

在不改变高度的情况下确实是可以直接在子线程修改UI的,那再来试下修改了高度会怎么样。这个时候我们将TextView的宽度设置小一点,让文案一行显示不下, 换行显示:

<?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=".MainActivity">
    <TextView
        android:id="@+id/tv_test"
        android:layout_width="30dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

再来看下结果:

image

结果也是意料之中了。这个时候TextView的内容需要换行显示,这个时候高度发生了变化,那么最终就会进入到checkThread里面去,然后报出错误


总结

其实想想看,这么设计也是合情合理的,既然TextView的宽高都保持不变,那么自然没必要在去调用requestLayout方法测量它的宽高了,优化了性能。只不过这样就直接导致了在子线程也可以修改文案。

相关文章

  • 009 采坑-子线程更新UI-2

    先看一下截图吧 onResume方法是自定义的,在系统onResume方法中调用,但是依然没有闪退。这个时候我的脑...

  • 009 采坑-子线程更新UI-1

    能在子线程里面修改UI?先别急,慢慢往下看: 举例 首先我们来看个例子: 上述代码就是新开了一个线程,然后在子线程...

  • Android消息机制(一)Handler

    利用Handler机制,子线程更新UI 使用上说:1.首先初始化一个handler 2.当子线程需要更新UI时,采...

  • Android不在子线程更新UI

    报错 UI 的线程检查机制就已经建立了,所以在子线程更新就会报错。 子线程更新的错误定位 子线程更新的错误定位是 ...

  • 线程通讯详解

    关于子线程能否更新UI的思考线程通讯详解线程池-多线程的高效使用姿势 上文我们说到了关于子线程中能否更新UI的问题...

  • Android在线程中更新UI和在协程中更新UI

    1、在子线程里面更新UI 我们都知道Android只能在主线程里面对UI更新,所以谷歌提供了很多在子线程里面更新U...

  • 如何做到在子线程更新 UI?

    一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。 但在某种情况下直接开启线程更新 UI 是不...

  • Android 的线程和线程池

    Android 的线程分为主线程和子线程。 主线程更新 UI 子线程执行耗时操作 AsyncTask封装了线程池和...

  • Android 关于子线程更新UI的那些事

    一 相信大家都有听过,子线程更新UI的操作。但这种说法,不是很明确。有些人说子线程更新UI会挂,而有些人说子线程可...

  • Android Handler探索

    在日常开发中,我们常用Handler来在子线程更新主线程UI,这是因为Android系统不允许我们在子线程更新UI...

网友评论

    本文标题:009 采坑-子线程更新UI-2

    本文链接:https://www.haomeiwen.com/subject/xdrvaktx.html