继续上一篇的Android可不可以在子线程中更新UI?https://www.jianshu.com/p/99a6aeba1161
今天再继续深入了解一个话题Android UI 线程更新UI是否也会崩溃?
我们先从一个小需求开始说起:
点击一个按钮,请求服务端获取一些文本信息在客户端用Dialog展示出来,然后再与对话框一些交互操作。
下面贴一下代码:



代码很简单,就是点击按钮,新启动一个线程去模拟网络请求,结果拿到后,把问题展示在Dialog。运行完毕,点击按钮直接崩溃:

说明在子线程执行Dialog崩溃:
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-3,5,main] that has not called Looper.prepare()
这个有点经验的安卓开发都知道,在子线程弹Dialog,需要初始化Looper,和启动Looper循环。

此时的运行效果是这样的,没有发生崩溃了。

下面开始引出我们的问题,仔细发现

箭头所指向的mTvTitle.setText(title);是在子线程更新title,居然一点也没报错。
boolean isUiThread=Looper.getMainLooper()==Looper.myLooper(); 这句判断是否在主线程日志中也打印的是false,另外也可以看PID19559-后面的数字是否和PID相等,不想等的话也是子线程
2022-08-06 01:15:14.190 19559-19689/? D/tanlin: false。
如果说是按照前篇文章所描述的那样 ViewRootImpl还没创建的话,这个时候肯定是创建的了我认为,我们是延迟一秒再更新的title。那么是什么原因导致没有报错呢?
接下来我添加一段代码逻辑,就是点击不是的时候,我就把文案在以前的基础上加上"犹豫中?"
看看效果。

效果也是正常的。
虽然此时没有崩溃,也没有任何错误日志,但我还是相信UI只能在UI线程更新以前一直这么认为的。

当我把它放在主线程中去更新时,我点击不是的时候点了三次结果反而崩溃了。

这就是回到了今天开始的主题:Android UI 线程更新UI也会崩溃:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
我们都很熟悉了这个错误。而且是我点了三下才崩溃的,没加上handler.post还是好好的。
现在我们来仔细分析这个错误:打开安卓的framework源码搜

从错误可以推断出mThread != Thread.currentThread()的,那么我们来看看mThread是在哪赋值的,

是在ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象。
也就是说,你ViewRootImpl是在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。
那么我们的Dialog其实是在子线程中创建的,Dialog的ViewRootImpl也就是在子线程中创建的,那么后续Dialog的更新也需要在子线程中。所以导致后续Dialog更新,执行到ViewRootImpl#checkThread的时候,都在子线程才可以。这就是说明了刚才我加上handler.post到UI线程却崩溃了,
再来想想,为什么我点第三次才崩溃,前两次都没有呢?它们有啥区别,是触发了什么事件吗?下面这张图是点击两次的时候,当再点一次就崩溃了。

想想第三次和第二次的TextView高度不一样了改变了。调用setTextView时高度发生改变了,就会触发了requestLayout。(补充一点:这里我宽度是match_parent。如果改为wrap_parent。点击一次也会崩溃。宽度改变了也会触发requesLayout)

所以当你的TextView触发requestLayout,会辗转到ViewRootImpl的requestLayout,然后再找到它的checkThread。而checkThread判断的mThread和当前线程对比。
具体TextView的宽高改变触发requestLayout时机,下次写篇文章源码继续分析。
网友评论