相信大家都有听过,子线程更新UI的操作。但这种说法,不是很明确。有些人说子线程更新UI会挂,而有些人说子线程可以更新UI。接下来分析下这两种情况。

先来说说子线程更新UI会挂的问题吧。

在Activity中onCreate完后,会生成一个ViewRootImpl,View的绘制都是同个它来实现的,而ViewRootImpl调用到requestLayout()来完成View的绘制操作。看下源码:

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

但布局绘制或者发生变化时,会调用requestLayout(),而里面有checkThread(),来看下它的源码:

//ViewRootImpl.java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

其中mThread是在ViewRootImpl创建时初始化的,把创建时的线程和mThread绑定,而ViewRootImpl又是在主线程初始化的,所以mThread表示主线程。假如更新UI,会调用requestLayout() -> checkThread() -> mThread != Thread.currentThread(),判断是否在主线程,如果在子线程中,会抛异常,所以子线程更新UI才会挂。

子线程更新UI会挂的思路明确了,再来看看子线程更新UI为什么不会挂吧。

刚才讲到了,在Activity中onCreate完后,会生成一个ViewRootImpl,那么之后它就会去检查你更新UI时在哪个线程。那假如我在onCreate时去开一个子线程更新UI,此时ViewRootImpl还没创建,就不会去检测UI变化,所以在onCreate中子线程是可以更新UI的。

那又有些人说,我不在onCreate里面更新,我就在onCreate后在子线程里面去更新,它还是不会挂,这又是为什么呢?主要还是看下更新UI的方法吧

text1.setOnClickListener {
thread {
it.post {
text1.text = "change"
}
}
}

这里用一个点击事件,开子线程更新UI,运行后发现没挂,这是为什么?看下post的源码:

//View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}

通过源码分析,你就知道这个mHandler是在ViewRootImpl里面赋值的,mHandler是主线程的Handler,而又掉了handler.post(),所以只要Handler在主线程,那么它post的所有的UI操作都是主线程。看起来像在子线程,实际是回到主线程更新UI。

继续看这种

class MyTestActivity:AppCompatActivity() {
private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ab)
}
override fun onResume() {
super.onResume()
getHandler()
}
private fun getHandler(){
thread {
handler.post {
text0.text = "change"
}
}
}
}

由于创建handler时是在主线程,所以这个handler是属于主线程的,所以其他的步骤就更上面的一样了。另一种handler的发消息更新的方式我就不写出来了,只要通过handler更新UI的,只要handler是主线程的,必定不会挂。之前有写过Handler使用的文章,有兴趣的同学可以看看~

还有一种就是调用runOnUiThread{}

class MyTestActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ab)
}
override fun onResume() {
super.onResume()
thread()
}
private fun thread(){
thread {
runOnUiThread {
text.text = "change"
}
}
}
}

顾名思义,在主线程运行,只要调了这个方法,所有操作都在主线程里面,所有只要的操作不会挂。

总结一点,并不是说子线程可以更新,仔细点说:子线程中可以在调用主线程的Handler去更新UI,或者子线程可以调用runOnUIThread{}切换到主线程更新UI。

之前还遇到一种特殊情况,在onResume中更新UI不会挂,要是在最后一行加上 view.requestLayout()

@Override
protected void onResume() {
super.onResume();
new Thread() {
@Override
public void run() {
super.run();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TextView view = findView(R.id.tv_content);
view.setText("我在子线程更新");
view.setBackgroundColor(Color.RED);
view.requestLayout();
}
}.start();
}

再试一下,就崩了

总结一点:实际上,就是只要你改view,不触发checkThread()就没事,而TextView的宽高不改变,也不会去触发requestLayout(),修改背景也同样。不会触发view的位置大小改变。当然,这种情况,不是每个版本的android都有用,还是要规范的去主线程更新UI。