某天早晨,群里有個小伙伴這樣問了一個問題:
XXX:為什么我的控件可以在子線程里面更新
我(不假思索):你是不是在onCreate里面開了一個子線程,然后更新了UI
XXX:好像是這樣。。
我:你試試將子線程沉睡5秒鐘時間,應該就會閃退了
XXX:我試試。
N分鐘以后......
XXX:我加了沉睡時間,還是不會閃退
我:讓我看一下截圖吧
他的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();
}
實際測試下來好像還是會閃退,這種情況才是我認為的現象。于是我把我的實驗在群里發了一遍
我:我試了一下,子線程修改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一下看下效果
竟然真的修改成功了!
這下就比較懵逼了,radioButton可以修改成功,難道radioButton做了什么特殊的處理么?隨手去翻了一下radioButton的源碼以及父類CompoundButton的源碼,發現并沒有特別之處。既然還是沒找到原因,那么就debug源碼看下具體的原因。
前面的流程一切正常,然后執行到checkForRelayout的時候就有問題了:
在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,那么什么時候滿足條件呢,需要具備以下幾個條件
- 寬度不是wrap_content的或者mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth
- mHint == null || mHintLayout != null
- 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();
看下這樣的運行結果
在不改變高度的情況下確實是可以直接在子線程修改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>
再來看下結果:
結果也是意料之中了。這個時候TextView的內容需要換行顯示,這個時候高度發生了變化,那么最終就會進入到checkThread里面去,然后報出錯誤
總結
其實想想看,這么設計也是合情合理的,既然TextView的寬高都保持不變,那么自然沒必要在去調用requestLayout方法測量它的寬高了,優化了性能。只不過這樣就直接導致了在子線程也可以修改文案。