某天早晨,群里有個(gè)小伙伴這樣問了一個(gè)問題:
XXX:為什么我的控件可以在子線程里面更新
我(不假思索):你是不是在onCreate里面開了一個(gè)子線程捂敌,然后更新了UI
XXX:好像是這樣艾扮。。
我:你試試將子線程沉睡5秒鐘時(shí)間占婉,應(yīng)該就會(huì)閃退了
XXX:我試試泡嘴。
N分鐘以后......
XXX:我加了沉睡時(shí)間,還是不會(huì)閃退
我:讓我看一下截圖吧
他的onResume方法是自定義的逆济,在系統(tǒng)onResume方法中調(diào)用酌予,但是依然沒有閃退。
這個(gè)時(shí)候我的腦子也是一篇懵逼的奖慌。如果是onCreate開了子線程抛虫,然后子線程立刻更新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();
}
實(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ì)話讓我更疑惑了宇色。沒有想到原因自然是寫代碼實(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一下看下效果
竟然真的修改成功了!
這下就比較懵逼了宣蠕,radioButton可以修改成功例隆,難道radioButton做了什么特殊的處理么?隨手去翻了一下radioButton的源碼以及父類CompoundButton的源碼抢蚀,發(fā)現(xiàn)并沒有特別之處镀层。既然還是沒找到原因,那么就debug源碼看下具體的原因皿曲。
前面的流程一切正常,然后執(zhí)行到checkForRelayout的時(shí)候就有問題了:
在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方法惶我,這里沒有執(zhí)行requestLayout方法,自然不會(huì)崩潰博投。
- 那么TextView是在什么地方執(zhí)行的requestLayout呢绸贡?
- 又是什么原因?qū)е聸]有執(zhí)行requestLayout方法呢?
我們先來(lái)看第一個(gè)問題:其實(shí)只要截圖中的兩個(gè)條件都沒有進(jìn)入就會(huì)執(zhí)行requestLayout方法
第二個(gè)問題:回答這個(gè)問題首先看下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è)條件
- 寬度不是wrap_content的或者mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth
- mHint == null || mHintLayout != null
- 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沒有什么關(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é)果
在不改變高度的情況下確實(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é)果:
結(jié)果也是意料之中了帝蒿。這個(gè)時(shí)候TextView的內(nèi)容需要換行顯示,這個(gè)時(shí)候高度發(fā)生了變化撕瞧,那么最終就會(huì)進(jìn)入到checkThread里面去陵叽,然后報(bào)出錯(cuò)誤
總結(jié)
其實(shí)想想看,這么設(shè)計(jì)也是合情合理的丛版,既然TextView的寬高都保持不變巩掺,那么自然沒必要在去調(diào)用requestLayout方法測(cè)量它的寬高了,優(yōu)化了性能页畦。只不過(guò)這樣就直接導(dǎo)致了在子線程也可以修改文案胖替。