原标题:震惊!Android子线程也能修改UI?
1月1日,特斯拉正式宣布,中国制造Model Y正式发售。特斯拉中国官网显示,Model Y长续航版起售价为33.99万元人民币,而此前为48.8万元人民币,下调了14.81万元人民币。此外,特斯拉Model Y Performance 高性能版起售价为36.99万元人民币,此前为53.5万元人民币,下调了16.51万元人民币。
2021年的第一个工作日来啦!希望大家在崭新的一年里,都能百尺竿头,更进一步!
本篇文章来自CDF_cc7d的投稿,分享了子线程更新UI的小案例,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
CDF_cc7d的博客地址:
https://www.jianshu.com/u/dd07890ebff0
/ 前言 /
某天早晨,群里有个小伙伴这样问了一个问题:
XXX:为什么我的控件可以在子线程里面更新
我(不假思索):你是不是在onCreate里面开了一个子线程,然后更新了UI
XXX:好像是这样。。
我:你试试将子线程沉睡5秒钟时间,应该就会闪退了
XXX:我试试。
N分钟以后......
XXX:我加了沉睡时间,还是不会闪退
我:让我看一下截图吧
他的onResume方法是自定义的,在系统onResume方法中调用,但是依然没有闪退。
这个时候我的脑子也是一篇懵逼的。如果是onCreate开了子线程,然后子线程立刻更新UI,那是不会出现闪退的。
具体原因这篇文章(https://www.jianshu.com/p/1b2ccd3e1f1f)有详细解释过。
但是沉睡5秒钟还是能修改成功,这就让我有点吃惊了。
/ 开始 /
所以我打算自己写一个demo试试看
@Override
protectedvoidonResume{
super.onResume;
newThread( newRunnable {
@Override
publicvoidrun{
SystemClock.sleep( 5000);
mTvTest.setText( "子线程修改UI");
}
}).start;
}
实际测试下来好像还是会闪退,这种情况才是我认为的现象。于是我把我的实验在群里发了一遍
我:我试了一下,子线程修改UI是会闪退的,你是怎么做到的
XXX:我再试试。
过了一段时间
XXX:奇怪了,我现在好像也试不出来了。。。
又过了一段时间
XXX:我用的是radioGroup+radioButton,然后修改的是radioButton的文案,可以在子线程里执行,weight设置为1,width设置为0。
上面这段对话让我更疑惑了。没有想到原因自然是写代码实验一下:
< androidx.constraintlayout.widget.ConstraintLayoutxmlns: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
protectedvoidonResume{
super.onResume;
newThread( newRunnable {
@Override
publicvoidrun{
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
privatevoidcheckForRelayout{
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的。然后我们再看下条件里面的代码:
intoldht = mLayout.getHeight;
intwant = mLayout.getWidth;
inthintWant = 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的宽高是固定的,那么再来看下高度不固定但是修改前后保持一致是否也是可以修改成功的:
< androidx.constraintlayout.widget.ConstraintLayoutxmlns: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
protectedvoidonResume{
super.onResume;
newThread( newRunnable {
@Override
publicvoidrun{
SystemClock.sleep( 5000);
mTvTest.setText( "子线程修改UI");
}
}).start;
看下这样的运行结果
在不改变高度的情况下确实是可以直接在子线程修改UI的,那再来试下修改了高度会怎么样。这个时候我们将TextView的宽度设置小一点,让文案一行显示不下, 换行显示:
< androidx.constraintlayout.widget.ConstraintLayoutxmlns: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方法测量它的宽高了,优化了性能。只不过这样就直接导致了在子线程也可以修改文案。返回搜狐,查看更多
责任编辑: