Android 在子線(xiàn)程中直接更新UI

老生常談的問(wèn)題了惭笑,大家都知道在子線(xiàn)程中更新ui會(huì)報(bào)CalledFromWrongThreadException,此異常在ViewRootImpl.checkThread()方法中拋出捺宗。換句話(huà)說(shuō)川蒙,在子線(xiàn)程中更新UI不觸發(fā)checkThread()可以正常更新畜眨。

ViewRootImpl.checkThread()

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
ViewRootImpl還未創(chuàng)建一定不會(huì)觸發(fā)checkThread()昼牛,那么問(wèn)題來(lái)了ViewRootImpl何時(shí)創(chuàng)建?

Android Activity康聂、Window贰健、View 有說(shuō),在ActivityThread.handleResumeActivity()方法中先回調(diào)Activity.onResume()恬汁,然后調(diào)用Activity.makeVisible()經(jīng)過(guò)一系列調(diào)用到ViewRootImpl.performTraversals()開(kāi)始繪制流程伶椿。既然ViewRootImpl在onResume()之后才初始化氓侧,那么在onResume()和onResume()之前都可以在子線(xiàn)程中更新UI甘苍,可以驗(yàn)證下看彼。

TextActivity

package com.chenxuan.jetpack

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*

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

        Thread(Runnable {
            tv.text = "call on Thread"
        }).start()
    }
}

activity_text.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=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

結(jié)果自然是正常運(yùn)行。嚴(yán)格來(lái)講并不算更新UI茁计,ViewRootImpl還未開(kāi)啟繪制流程星压,此處改變text只是改變了TextView中的變量值逊脯。

ViewRootImpl初始化后仍可更新UI竣贪,有限定條件演怎。

TextActivity

package com.chenxuan.jetpack

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_text.*

class TextActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_text)
        
        button.setOnClickListener {
            Thread(Runnable {
                tv.text = "call on Thread"
            }).start()
        }
    }
}

activity_text.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=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="update"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>

不出所料扇住,看異常堆棧調(diào)用到了ViewRootImpl.requestLayout(),內(nèi)部調(diào)用了ViewRootImpl.checkThread()

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

下面來(lái)尋找源頭,從TextView.setText()開(kāi)始

    public final void setText(CharSequence text) {
        setText(text, mBufferType);
    }

    public void setText(CharSequence text, BufferType type) {
        setText(text, type, true, 0);

        if (mCharWrapper != null) {
            mCharWrapper.mChars = null;
        }
    }

    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
            ...
            checkForRelayout();
    }

TextView.checkForRelayout()

    private void checkForRelayout() {
        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            ...
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

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

            requestLayout();
            invalidate();
        } else {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

checkForRelayout()方法中一堆判斷冯键,歸根結(jié)底TextView大小不改變不會(huì)觸發(fā)requestLayout()惫确。

View.requestLayout()

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //調(diào)用到ViewRootImpl.requestLayout()
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

固定TextView大小

activity_text.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=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="content"
        android:textColor="@android:color/holo_orange_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="update"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv" />
</androidx.constraintlayout.widget.ConstraintLayout>

再運(yùn)行一下陈肛,點(diǎn)擊按鈕正常更新句旱。

ViewRootImpl.checkThread()中注釋說(shuō)明判斷的是更新UI的線(xiàn)程和創(chuàng)建UI的線(xiàn)程是否相同,并非一直以來(lái)說(shuō)的Android UI主線(xiàn)程匾南。那么在子線(xiàn)程中創(chuàng)建View自然可以在這個(gè)子線(xiàn)程中更新UI。
package com.chenxuan.jetpack

import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.view.Gravity
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class TextActivity : AppCompatActivity() {
    lateinit var asyncText: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val wm :WindowManager= getSystemService(Context.WINDOW_SERVICE) as WindowManager

        Thread(Runnable {
            Looper.prepare()
            asyncText = TextView(this)
            asyncText.setBackgroundResource(android.R.color.darker_gray)
            asyncText.setTextColor(resources.getColor(android.R.color.holo_orange_light))
            asyncText.text = "asyncText"
            val param = WindowManager.LayoutParams()
            param.gravity = Gravity.CENTER
            param.width = WindowManager.LayoutParams.WRAP_CONTENT
            param.height = WindowManager.LayoutParams.WRAP_CONTENT
            wm.addView(asyncText,param)
            Looper.loop()
        }).start()
    }
}

子線(xiàn)程中初始化TextView并設(shè)置各種屬性,通過(guò)WindowManager添加進(jìn)根布局。ViewRootImpl中初始化了ViewRootHandler逻住,在子線(xiàn)程中記得手動(dòng)創(chuàng)建Looper瞎访。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拾徙,更是在濱河造成了極大的恐慌尼啡,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寺惫,居然都是意外死亡西雀,警方通過(guò)查閱死者的電腦和手機(jī)腔呜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人丑孩,你說(shuō)我怎么就攤上這事温学。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我挺尾,道長(zhǎng)遭铺,這世上最難降的妖魔是什么魂挂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮炎码,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矫钓。我一直安慰自己,他們只是感情好概龄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布救欧。 她就那樣靜靜地躺著铝耻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泡态。 梳的紋絲不亂的頭發(fā)上桐汤,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天亮钦,我揣著相機(jī)與錄音蜡娶,去河邊找鬼窖张。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的副女。 我是一名探鬼主播碑幅,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼异吻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起春宣,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嚷辅,失蹤者是張志新(化名)和其女友劉穎扁位,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡爽雄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡穿扳,死狀恐怖衩侥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矛物,我是刑警寧澤茫死,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站履羞,受9級(jí)特大地震影響峦萎,放射性物質(zhì)發(fā)生泄漏屡久。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一爱榔、第九天 我趴在偏房一處隱蔽的房頂上張望被环。 院中可真熱鬧,春花似錦详幽、人聲如沸筛欢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)悴能。三九已至,卻和暖如春雳灾,著一層夾襖步出監(jiān)牢的瞬間漠酿,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工谎亩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炒嘲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓匈庭,卻偏偏與公主長(zhǎng)得像夫凸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阱持,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容