某天早晨,群里有个小伙伴这样问了一个问题:
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。
上面这段对话让我更疑惑了。没有想到原因自然是写代码实验一下:
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">
android:id="@+id/rg_group"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
android:id="@+id/rb_test1"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
android:text="这是第一个radiobutton"/>
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
android:text="这是第二个radiobutton"/>
布局文件如上写完,然后写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,那么什么时候满足条件呢,需要具备以下几个条件
宽度不是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的宽高是固定的,那么再来看下高度不固定但是修改前后保持一致是否也是可以修改成功的:
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">
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"/>
@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的宽度设置小一点,让文案一行显示不下, 换行显示:
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">
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"/>
再来看下结果:
image4.gif
结果也是意料之中了。这个时候TextView的内容需要换行显示,这个时候高度发生了变化,那么最终就会进入到checkThread里面去,然后报出错误
总结
其实想想看,这么设计也是合情合理的,既然TextView的宽高都保持不变,那么自然没必要在去调用requestLayout方法测量它的宽高了,优化了性能。只不过这样就直接导致了在子线程也可以修改文案。