android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8066)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1297)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
相信大家都见过这样的错误,而且拍大腿都知道,这个问题肯定是更新控件不在UI线程导致的。
你肯定有各种方式来解决。
但是我们今天就要问点你怀疑人生的问题:
- 真的只有UI线程才能更新界面UI吗?
- UI 线程更新界面UI有可能报上述错误吗?
- 请举例说明...
- 最好能附带源码解析。
更多问答 >>
-
每日一问 “PathClassLoader 只能加载已安裝到系統中(即/data/app目录下)的apk文件” 严谨吗?
2020-05-05 20:46 -
2020-05-07 10:02
-
每日一问 | Activity 调用了finish()方法会立即调用onDestory()吗?
2020-05-13 00:16 -
2020-05-21 01:15
-
每日一问 | 我们常说的dalvik虚拟机是基于寄存器的,而jvm是基于栈,到底指的是什么?
2020-05-20 21:29 -
2020-04-13 23:58
-
每日一问 | 我们经常用的 String类型,你知道它最大可以放多长的字符串吗?
2020-04-08 23:58 -
每日一问 | 上周出现了大规模的github证书不可用的状态...但是真的是github服务器被攻击了么?
2020-04-01 21:49 -
每日一问 | Fragment 是如何被存储与恢复的? 有更新
2020-06-07 09:01 -
每日一问 | Activity 启动动画对页面打开速度有影响吗?
2020-04-22 22:06
在平时的Android开发中,如果一个新手遇到一个这样的错:
你作为一只老鸟,嘴角露出一丝微笑:
“小兄弟,你这个是没有在UI线程执行UI操作导致的错误,你搞个UI线程的handler.post一下就好了”。
但是...
我今天要说,真是是只有UI线程才能更新UI吗?
你作为一只老鸟,肯定立马脑子里闪过:
我知道你这文章写啥了,又要在Activity#onCreate,去搞个线程执行TextView#setText,然后发现更新成功了,是不是?
这多年以前我就看过这样的文章,ViewRootImpl还没创建而已。
看你们这么强,我这个文章没法写下去了...
但是我这个人专治各种不服好吧,我换个问题:
UI线程更新UI就不会出现上面的错误了吗?
好了,开讲。
下面是一个应届小哥小奇写需求的故事。
小哥的需求
需求很简单,就是
是不是很简答。
小哥怒写一波代码:
很简单吧,点击按钮,新启动一个线程去模拟网络请求,结果拿到后,把问题展示在Dialog。
下面开始写Dialog的代码:
很简答,就一个标题,两个按钮。
然后我们在showQuestionInDialog让它show出来。
你们猜结果怎么着...
崩溃了...
第一次崩溃
应届生小齐迎来了第一次工作中的崩溃...
我们先停下来。
上面的代码很简单吧,那么我想问各位为什么会崩溃呢?凭各位多年的经验。
猜想:
上面new Thread模拟数据,没有切到UI线程就show Dialog了,而且执行了TextView#setText,肯定是在非UI线程更新UI导致的。
很有道理,绝不是一个人会这么猜测吧。
下面我们看真正报错的原因:
Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
虽然猜错了,但是依旧有点熟悉的感觉,以前大家在子线程弹toast的时候是不是见过类似的错误。
作为一个老鸟,遇到这个问题,肯定是不在UI线程弹Dialog,但是应届小哥就不同了。
瞎猫遇到死耗子
小哥,直接把报错信息扔进Google,不,百度:
点开第一篇CSDN的博客:
然后迅速举一反三,在刚才show Dialog的方法中增加:
解决问题就是这么简单,嘴角露出一丝对自己满意的笑容。
再次运行App...
这里大家再停一下。
凭各位多年的经验,我想再问一句,这次还会崩溃吗?
会吗?
猜想:
这代码治标不治本,还是没有在UI线程执行相关代码,还是会崩,而却刚才的show里面还有TextView#setText操作
有点道理。
看一下运行效果:
没有崩溃...
是不是有一丝的郁闷?
没关系,作为拥有多年经验的老鸟,总能立马想到解释的理由:
大家都知道在Activity#onCreate的时候,我们开个线程去执行Text#setText也不会崩溃,原因是ViewRootImpl那时候还没初始化,所以这次没崩溃也是一个原因。
对应源码解释是这样的:
我们首次创建的Dialog,第一次调用show方法,内部确实会执行mWindowManager.addView,这个代码会执行到:
这个mGlobal对象是WindowManagerGlobal,我们看它的addView方法:
果然立马有new ViewRootImpl的代码,你看ViewRootImpl没有创建,所以这和Activity那个是一个情况。
好像有那么点道理哈...
我们继续往下看。
应届小哥要继续做需求了。
一个隐藏的问题
接下来的需求很奇怪,就是当询问"鸿洋帅气吗?"的时候,如果你点击不是,那么Dialog不消失,在问题的末尾再加一个?号,如此循环,永不关闭。
这难不倒我们的小哥:
运行效果:
很完美。
如果我问,你觉得这个代码有问题吗?
你往上看了几眼,就这两行代码有个鸡儿问题,可能有空指针?
当然不是。
我稍微修改一下代码:
每次点击的时候,我弹了个Toast,输出当前线程是不是UI线程。
看下效果:
发现问题了吗?
出乎自己的意料吗?
我们在非UI线程一直在更新TextView的text。
这个时候,你不能跟我扯什么ViewRootImpl还没有创建了吧?
别急...
还有更刺激的。
更刺激的事情
我再改一下代码:
我搞了个UI线程的handler,然后post一下Runnable,确保我们的TextView#setText在UI线程执行,严谨而又优雅。
再停一下,以各位多年经验,这次会崩溃吗?
按照我写博客的套路,这次肯定是演示崩溃呀,不然博客怎么往下写。
好像是这个道理...
我们跑一下效果:
点击了几下,没崩...
// 配图:小朋友,你是不是有很多问号。
作为拥有多年经验的老鸟,总能立马想到解释的理由:
UI线程更新崩溃呀(言语中有一丝不自信)。
是吗?
我们多点击几次:
崩溃了...
但是刚才在没有添加UiHandler.post之前可没有崩溃哟。
好了,再停一停。
我又要问大家一个问题了,这次你猜是什么崩溃?
是不是求我别搞你们了,直接揭秘吧。
那个熟悉的身影回来了:
但是!
但是!
这次可是在切换到UI线程抛出来的。
对应我开头的灵魂拷问:
UI线程更新UI就不会出现上面的错误了吗?
是不是在一股懵逼又刺激的感觉中无法自拔...
别怕,没完,我总得告诉你们为什么吧。
小做揭秘
其实这一切的根源都在于我们长久的一个错误的概念。
就是UI线程才能UI线程,这是不对的,为什么这么说呢?
这个异常是在ViewRootImpl里面抛出的对吧,我们再次来审视一下这段代码:
其实就几行代码。
我们仔细看一下,他这个错误信息并不是:
Only the UI Thread ... 而是 Only the original thread。
对吧,如果真的想强制为Only the Ui Thread,上面的if语句应该写成:
而不是mThread。
根本原因说完了。
我再带大家看下源码解析:
这个mThread是什么?
是ViewRootImpl的成员变量,我们重点应该关注它什么时候赋值的:
在ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象。
也就是说,你ViewRootImpl在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。
对应到上面的例子,我们中间也有段贴源码的地方。
恰好说明了:
Dialog的ViewRootImpl,其实是在执行show()方法的时候创建的,而我们的Dialog的show放在子线程里面,所以导致后续View更新,执行到ViewRootImpl#checkThread的时候,都在子线程才可以。
这就说明了,为什么我们刚才切到UI线程去执行TextView#setText为啥崩了。
大家可能还有个一问题:
ViewRootImpl怎么和View关联起来的
其实我们看报错堆栈很好找到相关代码:
报错的堆栈都是由View.requestLayout触发到ViewRootImpl的。
我们直接看这个方法:
注意里面这个mParent变量,它的类型是ViewParent接口。
见名知意。
我要问你一个View的mParent是什么,你肯定会回答是它的父View,也就是个ViewGroup。
对,没错。
ViewGroup确实实现了ViewParent接口。
但是还有个问题,一个界面的最最最上面那个ViewGroup它的mParent是谁?
对吧,总不能还是ViewGroup吧,那岂不是没完没了了。
所以,ViewParent还有另外一个实现类,叫做ViewRootImpl。
现在明白了吧。
按照ViewParent的体系,我们的界面结构是这样的。
嗯,我还是写坨代码吧:
还是刚才Dialog,当我们点击No的时候,我们打印下ViewParent体系:
很简单,我们就打印mTbTitle,一直往上的ViewParent体系。
看到没,最底部的是谁。
是它,是它,就是它,我们的ViewRootImpl。
所以当你的TextView触发requestLayout,会辗转到ViewRootImpl的requestLayout,然后再到它的checkThread,而checkThread判断的并非是UI线程和当前线程对比,而是mThread和当前线程对比。
到这里,我可以结尾了吧。
下一篇我可能要写:Google好像在秀我们,欢迎关注等文,具体时间未定,思路暂无。
也欢迎关注我的公众号,微信搜索「鸿洋」,拜了个拜!
嗯,排版显示还可以。
尴尬,确实想不明白为什么最后需要点好几次才会导致crash,肯定不应该是主线程的msg太多了,排不上post的runnable…… 你强调的xml中哪个属性需要留意保持一致,也没看出来…… 鸿神再给些 ...查看更多
尴尬,确实想不明白为什么最后需要点好几次才会导致crash,肯定不应该是主线程的msg太多了,排不上post的runnable…… 你强调的xml中哪个属性需要留意保持一致,也没看出来…… 鸿神再给些指点吧,虽然张口要不好,大晚上回来的,有点(懒)不想折腾demo了 以前你推送过几篇类似的,只要是有单独window的,比如toast,会addview时生成单独的viewrootImpl,就会有类似的可能。
不是msg过多的问题,看看TextView.setText()方法。TextView layout属性为WRAP_CONTENT或MATCH_PARENT,布局改变,对应洋神+?导致换行,触发了Tex ...查看更多
不是msg过多的问题,看看TextView.setText()方法。TextView layout属性为WRAP_CONTENT或MATCH_PARENT,布局改变,对应洋神+?导致换行,触发了TextView.requestLayout(),调用到父类ViewRootImpl.requestLayout(),而checkThread()正是在requestLayout()中调用。也就是说TextView大小固定是不会崩溃的,因为不会走checkThread()
楼内好像不能贴代码,TextView. setText()->checkForRelayout()->View.requestLayout()->ViewRootImpl.reque ...查看更多
楼内好像不能贴代码,TextView. setText()->checkForRelayout()->View.requestLayout()->ViewRootImpl.requestLayout() WRAP_CONTENT、MATCH_PARENT的判断在checkForRelayout()方法中
你也太好心了,感谢。确实,TextView.requestLayout()的触发有条件。我再提个疑问,看来在crash之前 invalidate的时候,在 invalidate -> sched ...查看更多
你也太好心了,感谢。确实,TextView.requestLayout()的触发有条件。我再提个疑问,看来在crash之前 invalidate的时候,在 invalidate -> scheduleTraversals -> doTraversal -> doTraversal 这个流程,不检测 checkThread? 这个我随便问的,有空我试试,再次感激
invalidate方法不会触发checkThread
问题 1:
其实任何线程都可以更新自己创建的 UI。只要保证满足下面几个条件就好了
问题 2:UI 线程更新界面UI有可能报上述错误吗?
有可能的,如果在 UI 线程操作不是在 UI 线程创建的控件,也会抛出上述的错误。
写了一篇文章,内附 demo,
详见 https://juejin.im/post/5e9b0cede51d4546c1644fc1点赞,我也写了篇,周一会推送,相互学习~
洋神的表述特别有趣,学习了~
老哥标题好像不太对,满足下面几个条件,可是下面的条件都是独立的,并不是关联的呀
肯定是不对的,我直接把我笔记复制过来了。
众所周知安卓不允许在非UI线程中去更新UI,每当我们对View状态做出改变的时候(如调用requestLayout()或invalidate()等方式时)都会去检查当前线程是否是主线程,而检查线程的判断是在ViewRootImpl的checkThread()方法中去执行的。也就是说在ViewRootImpl没有创建出来的时候(OnResume执行完后wm.addView(decor, l)后ViewRootImpl才创建出来的)checkThread()这一步检测是不会执行的,在这种情况下我们在子线程中是可以更新UI的。如果我们在异步线程中去创建一个ViewRootImpl对象的话,同样可以在异步线程中去更新View,因为ViewRootImpl的checkThread()去检测线程时是通过比对当前ViewRootImpl所创建的线程和当前线程是否相同确定是否是主线程的,而ViewRootImpl默认是在主线程中创建的,所以是判断是否在主线程中去更新了View,如果在异步线程中创建了ViewRootImpl对象的话,就可以在异步线程更新UI了。
在异步线程创建ViewRootImpl时首先要绑定Looper,因为ViewRootImpl内部有一个handler,不绑定会出现找不到Looper的异常,且必须通过windowManager.addView的方式去创建ViewRootImpl 对象。ViewRootImpl被@hide注解标注,无法new出来。