震惊!Android子线程也能修改UI?(第二篇)

某天早晨,群里有个小伙伴这样问了一个问题:
XXX:为什么我的控件可以在子线程里面更新
我(不假思索):你是不是在onCreate里面开了一个子线程,然后更新了UI
XXX:好像是这样。。
我:你试试将子线程沉睡5秒钟时间,应该就会闪退了
XXX:我试试。
N分钟以后......
XXX:我加了沉睡时间,还是不会闪退
我:让我看一下截图吧

image.png

他的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();
    }
image1.gif

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


我:我试了一下,子线程修改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一下看下效果

image2.gif

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

在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();

看下这样的运行结果


image3.gif

在不改变高度的情况下确实是可以直接在子线程修改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>

再来看下结果:


image4.gif

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


总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容

  • 看到标题我想大部分人会觉得我是标题党,怎么可能在子线程里面修改UI。先别急,慢慢往下看: 举例 首先我们来看个例子...
    CDF_cc7d阅读 4,645评论 2 14
  • 问题引入 Android 开发法则之一不能在子线程更新 UI,这个问题主要是 Android 关于 View 的一...
    JzyCc阅读 691评论 0 2
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,523评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,562评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,726评论 1 1