2019-07-05:子线程能否更新UI?为什么?
Moosphan opened this issue · 22 comments
可以更新UI但是不推荐,因为很容易造成carsh。因为Android会checkThread,可以尝试一下在子线程更新setText(),文字是会改变的,但紧接着就会报异常。
很早我们就被灌输一个既定事实:子线程不能够更新UI。然而,UI真的无法在子线程更新吗?为什么?这里的题意可能会被误解,所以备注一下。
不可以,因为会出现异常情况;在子线程中更新完后会出现异常现象如下:
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)
需要在子线程中发送一个message到主线程handlemessage中进行主线程去更新ui控件
子线程是不能直接更新UI的
注意这句话,是不能直接更新,不是不能更新(极端情况下可更新)
绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制ui,极端情况就是,在Activity的onResume(含)之前的生命周期中子线程都可以进行更新ui,也就是 onCreate,onStart和onResume,此时主线程的绘制还没开始。
不能直接更新,更新方式有三种
1、
new Handler(mContext.getMainLooper()).post(new Runnable() { @Override public void run() { mTextView.setText("update text not in UI Thread"); } });
2、
`((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
// 在这里执行你要想的操作 比如直接在这里更新ui或者调用回调在 在回调中更新ui
}
});`
3、
handler发送message给UI线程更新
不能直接更新Ui,直接更新会崩溃,但你更改的内容还是会变化的,因为你子线程更新了,但此时主线程不知道,主线程用来绘制ui,所以会崩溃。下面方法可以用来在子线程更新UI
- new Handler(getMainLooper).post(new Runnable(){}
- runOnUiThread(new Runable){}
- 通过handler发送message来更新Ui
其实runOnUiThread内部就是调用new Handler(getMainLooper).post,而new Handler(getMainLooper).post内部调用的是sendMessageDelayed,所以更新Ui的原理就是通过handler发送message
这句话应该改成: 不能更新非本线程创建的View
这句话应该改成: 不能更新非本线程创建的View
源码里面的注释是只能创建view的线程去更新该view,所以理论上是可以子线程创建,然后子线程更新的,但有个问题:子线程创建的View可以不通过主线程显示吗?如果不能越过主线程显示,那结果等价于子线程不能更新UI,因为不能被看到的View毫无意义啊。
这句话应该改成: 不能更新非本线程创建的View
源码里面的注释是只能创建view的线程去更新该view,所以理论上是可以子线程创建,然后子线程更新的,但有个问题:子线程创建的View可以不通过主线程显示吗?如果不能越过主线程显示,那结果等价于子线程不能更新UI,因为不能被看到的View毫无意义啊。
后来想了想,我说的这句话也不对,为何不能更新主线程的view,源自于 ViewRootImp 对线程做了校验,那么如果 创建一个View,不显示出来,也就没有了ViewRootImp中的线程校验,所以略尴尬=.=
也就是说,如果一个View没有被add到 window中,其实就是通过ViewRootImp.setView 添加到window中,那么就不存在 线程校验这回事
那么我们什么时候需要一个不存在于页面上的View呢??? 我目前最常用的是拼图=.=,生成一个view 然后截图
这句话应该改成: 不能更新非本线程创建的View
源码里面的注释是只能创建view的线程去更新该view,所以理论上是可以子线程创建,然后子线程更新的,但有个问题:子线程创建的View可以不通过主线程显示吗?如果不能越过主线程显示,那结果等价于子线程不能更新UI,因为不能被看到的View毫无意义啊。
后来想了想,我说的这句话也不对,为何不能更新主线程的view,源自于 ViewRootImp 对线程做了校验,那么如果 创建一个View,不显示出来,也就没有了ViewRootImp中的线程校验,所以略尴尬=.=
也就是说,如果一个View没有被add到 window中,其实就是通过ViewRootImp.setView 添加到window中,那么就不存在 线程校验这回事
那么我们什么时候需要一个不存在于页面上的View呢??? 我目前最常用的是拼图=.=,生成一个view 然后截图
再补充一点,有一种 子线程可以更新主线程View的方法就是 在view被添加进window之前,应该是在onResume之前吧,忘记了,有兴趣的可以看看activity 启动源码
我翻到了上面有个哥子发了篇博客,博客里说到了更新ui时通常会调用requestLayout方法,最终调用到ViewRootImpl的requestLayout方法。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
只要条件判断不成立,就能跳过线程检查的步骤。
然后博主搞了个*操作,先在主线程更新UI,使得performLayout设置mHandlingLayoutInLayoutRequest为true,然后趁着主线程更新状态还没完成,马上又去用子线程更新UI,成功了。
这个无疑是对更新逻辑的利用,但是仍旧无法实现直接在子线程直接更新UI,也并没有回答让子线程创建的View在window中显示出来,并且还能更新UI的问题。
现在暂时没空去花时间仔细研究个结果,但上面的话无疑是对更新UI的知识的推进,所以贴了上来。
有进一步推进理解的,请指教。
checkThread发送在viewRootImpl,其在onResume时创建 子线程在初始化looper且looper.loop开启循环 把子线程运行在oncreate时机更新UI也应该可以
只要操作的View没有被添加到ViewRootImpl中,可以随意在任何线程中操作UI
子线程能更新UI,但是不推荐这么做。一般说的子线程不能更新UI,是因为执行更新UI操作的时候会进行checkThread检查,checkThread判断如果当前线程不是UI线程就会抛出异常。而checkThread跟ViewRootImpl这个类的对象有关,那么只要ViewRootImpl的对象还未创建,就无法执行checkThread,也就是在子线程更新UI也不会报错。ViewRootImpl的对象是在onResume()之后创建的,因此在onCreate()、onStart()、onResume()中可以做子线程更新UI
在子线程里更新TextView内容有以下两种场景是不会抛出异常的:
1、TextView还没来得及加入到ViewTree中,比如在Activity onResume执行之前。
2、开启了硬件加速的条件下,已经在ViewTree中的TextView被设置了固定的宽高,或者更新的内容不会导致控件宽高变化,即不会触发重新布局。
结论:在子线程操作View 确实不一定导致Crash,那是因为刚好满足一定的条件没有触发checkThread机制,但这并不代表我们在开发过程中可以这么写。
子线程是不能用于更新UI的 在子线程更新UI会导致应用崩溃 如果想更新UI就要切换到主线程来更新
子线程可以在ViewRootImpl还没有被创建之前更新UI;
访问UI是没有加对象锁的,在子线程环境下更新UI,会造成不可预期的风险;
开发者更新UI一定要在主线程进行操作;
Android:为什么子线程不能更新UI
发散一下思维 , Android禁止在子线程更新UI的根本原因是为了维持单线程更新UI,为什么不能多线程更新UI需要认真考虑其缺点和优点
这句话应该改成: 不能更新非本线程创建的View
源码里面的注释是只能创建view的线程去更新该view,所以理论上是可以子线程创建,然后子线程更新的,但有个问题:子线程创建的View可以不通过主线程显示吗?如果不能越过主线程显示,那结果等价于子线程不能更新UI,因为不能被看到的View毫无意义啊。
想想 Toast,Toast 可不可以在子线程弹出呢,答案是可以的,Toast 中 View 创建的时候所处的线程是子线程,弹出时进行checkThread 检查也会直接通过,最后添加到 wms 中,就可以显示出来