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

某天早晨,群里有個(gè)小伙伴這樣問(wèn)了一個(gè)問(wèn)題:
XXX:為什么我的控件可以在子線程里面更新
我(不假思索):你是不是在onCreate里面開(kāi)了一個(gè)子線程,然后更新了UI
XXX:好像是這樣。。
我:你試試將子線程沉睡5秒鐘時(shí)間,應(yīng)該就會(huì)閃退了
XXX:我試試。
N分鐘以后......
XXX:我加了沉睡時(shí)間,還是不會(huì)閃退
我:讓我看一下截圖吧

image.png

他的onResume方法是自定義的,在系統(tǒng)onResume方法中調(diào)用,但是依然沒(méi)有閃退。
這個(gè)時(shí)候我的腦子也是一篇懵逼的。如果是onCreate開(kāi)了子線程,然后子線程立刻更新UI,那是不會(huì)出現(xiàn)閃退的。具體原因這篇文章有詳細(xì)解釋過(guò)。但是沉睡5秒鐘還是能修改成功,這就讓我有點(diǎn)吃驚了。


所以我打算自己寫一個(gè)demo試試看

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

實(shí)際測(cè)試下來(lái)好像還是會(huì)閃退,這種情況才是我認(rèn)為的現(xiàn)象。于是我把我的實(shí)驗(yàn)在群里發(fā)了一遍


我:我試了一下,子線程修改UI是會(huì)閃退的,你是怎么做到的
XXX:我再試試。
過(guò)了一段時(shí)間
XXX:奇怪了,我現(xiàn)在好像也試不出來(lái)了。。。
又過(guò)了一段時(shí)間
XXX:我用的是radioGroup+radioButton,然后修改的是radioButton的文案,可以在子線程里執(zhí)行,weight設(shè)置為1,width設(shè)置為0。


上面這段對(duì)話讓我更疑惑了。沒(méi)有想到原因自然是寫代碼實(shí)驗(yàn)一下:

<?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="這是第一個(gè)radiobutton"/>
        <RadioButton
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="這是第二個(gè)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的源碼,發(fā)現(xiàn)并沒(méi)有特別之處。既然還是沒(méi)找到原因,那么就debug源碼看下具體的原因。
前面的流程一切正常,然后執(zhí)行到checkForRelayout的時(shí)候就有問(wèn)題了:
image.png

在checkForRelayout的方法里面,radioButton最終執(zhí)行了invalidate方法直接return掉了。根據(jù)這篇文章可知我們拋出Only the original thread that created a view hierarchy can touch its views.這個(gè)異常是在checkThread方法里面,而checkThread是由于調(diào)用了requestLayout方法,這里沒(méi)有執(zhí)行requestLayout方法,自然不會(huì)崩潰。

  • 那么TextView是在什么地方執(zhí)行的requestLayout呢?
  • 又是什么原因?qū)е聸](méi)有執(zhí)行requestLayout方法呢?
    我們先來(lái)看第一個(gè)問(wèn)題:其實(shí)只要截圖中的兩個(gè)條件都沒(méi)有進(jìn)入就會(huì)執(zhí)行requestLayout方法
    第二個(gè)問(wèn)題:回答這個(gè)問(wèn)題首先看下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();
        }
    }

首先看下最外層的判斷條件,條件如果滿足的時(shí)候就不會(huì)執(zhí)行requestLayout,那么什么時(shí)候滿足條件呢,需要具備以下幾個(gè)條件

  1. 寬度不是wrap_content的或者mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth
  2. mHint == null || mHintLayout != null
  3. mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)
    其實(shí)這三個(gè)條件同時(shí)滿足時(shí)就可以證明當(dāng)前的View寬度是固定的并且寬度值是大于0的。然后我們?cè)倏聪聴l件里面的代碼:
           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();

要想不執(zhí)行requestLayout方法,那么我們首先必須滿足(mEllipsize != TextUtils.TruncateAt.MARQUEE)條件表明當(dāng)前TextView并不是走馬燈的形式。然后進(jìn)入接下來(lái)的條件

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

這個(gè)條件要求我們?nèi)绻叨仁枪潭ㄖ档脑捘敲淳筒粫?huì)執(zhí)行requestLayout方法了。那么如果高度不是固定值怎么辦呢?接下來(lái)看下面的邏輯

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

當(dāng)前View的高度等于修改UI之前的高度并且HintLayout等于空或者是HintLayout的高度也等于修改UI之前的高度,那么就不會(huì)執(zhí)行requestLayout。什么意思呢?就是說(shuō)即便高度是不固定的,但是只要修改前后高度一致,那么一樣不會(huì)調(diào)用requestLayout。


這么看來(lái)只要View的寬度和高度在修改前后保持不變那么應(yīng)該就不會(huì)去做requestLayout的,也就是說(shuō)跟RadioButton沒(méi)有什么關(guān)系,只是恰好這么設(shè)置以后radioButton的寬高是固定的,那么再來(lái)看下高度不固定但是修改前后保持一致是否也是可以修改成功的:

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

看下這樣的運(yùn)行結(jié)果


image3.gif

在不改變高度的情況下確實(shí)是可以直接在子線程修改UI的,那再來(lái)試下修改了高度會(huì)怎么樣。這個(gè)時(shí)候我們將TextView的寬度設(shè)置小一點(diǎn),讓文案一行顯示不下, 換行顯示:

<?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>

再來(lái)看下結(jié)果:


image4.gif

結(jié)果也是意料之中了。這個(gè)時(shí)候TextView的內(nèi)容需要換行顯示,這個(gè)時(shí)候高度發(fā)生了變化,那么最終就會(huì)進(jìn)入到checkThread里面去,然后報(bào)出錯(cuò)誤


總結(jié)

其實(shí)想想看,這么設(shè)計(jì)也是合情合理的,既然TextView的寬高都保持不變,那么自然沒(méi)必要在去調(diào)用requestLayout方法測(cè)量它的寬高了,優(yōu)化了性能。只不過(guò)這樣就直接導(dǎo)致了在子線程也可以修改文案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 看到標(biāo)題我想大部分人會(huì)覺(jué)得我是標(biāo)題黨,怎么可能在子線程里面修改UI。先別急,慢慢往下看: 舉例 首先我們來(lái)看個(gè)例子...
    CDF_cc7d閱讀 4,803評(píng)論 2 14
  • 問(wèn)題引入 Android 開(kāi)發(fā)法則之一不能在子線程更新 UI,這個(gè)問(wèn)題主要是 Android 關(guān)于 View 的一...
    JzyCc閱讀 779評(píng)論 0 2
  • 久違的晴天,家長(zhǎng)會(huì)。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí),離放學(xué)已經(jīng)沒(méi)多少時(shí)間了。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,866評(píng)論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開(kāi)了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    余生動(dòng)聽(tīng)閱讀 10,912評(píng)論 0 11
  • 可愛(ài)進(jìn)取,孤獨(dú)成精。努力飛翔,天堂翱翔。戰(zhàn)爭(zhēng)美好,孤獨(dú)進(jìn)取。膽大飛翔,成就輝煌。努力進(jìn)取,遙望,和諧家園??蓯?ài)游走...
    趙原野閱讀 3,542評(píng)論 1 1

友情鏈接更多精彩內(nèi)容