为什么 Android 必须在主线程更新 UI ?

2370次阅读  |  发布于2年以前

为什么Android必须在主线程更新UI?

站在各位大牛的肩膀上,谢谢!

正常情况下,Android需要在UI线程更新UI,然鹅,在特殊情况下,子线程也能更新UI不在讨论之列,可参考[Android中子线程真的不能更新UI吗] ?这篇文章主要讲一下个人理解的正常情况下为什么不能在非UI线程更新UI。

先拿一句话来镇楼

android.view.ViewRootImpl$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

然后晒出Android官方的一句话来说:“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.” 因为Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。我们就主要分析一下这句话背后包含的含义。

Android屏幕刷新机制。

1, 界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务;

2, scheduleTraversals() 会先过滤掉同一帧内的重复调用,在同一帧内只需要安排一次遍历绘制 View 树的任务即可,这个任务会在下一个屏幕刷新信号到来时调用 performTraversals() 遍历View 树,遍历过程中会将所有需要刷新的 View 进行重绘;

3接着 scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作;

4 发完同步屏障后 scheduleTraversals() 才会开始安排一个遍历绘制 View 树的操作,作法是把 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法;

5,postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法;

6, native 层的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app;

7,onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的消息,这个消息是异步消息,所以不会被同步屏障拦截住;

8,doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作;

9,上述第4步到第8步涉及到的消息都手动设置成了异步消息,所以不会受到同步屏障的拦截;

10,doTraversal() 方法会先移除主线程的同步屏障,然后调用 performTraversals() 开始根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View;


View刷新流程时序图

详细信息可参考Android 屏幕刷新机制

文章说的很详细,简单来说,
就是当View的刷新操作触发时,会统一先注册到ViewRootImpl中;
屏幕每隔16.6ms触发一次刷新,这个信号会通知ViewRootImpl进行UI刷新,
然后在ViewRootImpl中实际执行View的测量,绘制的一系列操作

二,UI线程到底是什么

在上述4-8步中,是在某个线程完成的,这个线程就是实际上的UI线程。UI线程的名字的意义是,遍历View树,测量绘制View,并将数据写入到buffer的线程。在一个APP启动的时候,会建立一个Main Thread,这时候仍要绘制页面,因此这个Main Thread和UI Thread就是同一个线程。所以Main Thread和UI Thread相当于同一个概念。

三,为什么说必须UI线程更新UI

对于开发来说的更新UI,实际上是将View的变化,通知到ViewRootImpl,由ViewRootImpl实现后续操作。这个通知ViewRootImpl的操作包括
1,invalidate(请求重绘)
2,requestLayout(重新布局)
3,requestFocus(请求焦点)
4,startActivity(打开新界面)
5,onRestart(重新打开界面)
6,KeyEvent(遥控器事件,本质上是焦点导致的刷新)
7,Animation(各种动画,本质上是请求重绘导致的刷新)
8,RecyclerView滑动(页面滑动,本质上是动画导致的刷新)
9,setAdapter(各种adapter的更新)
10,…………
这些操作所在的线程必须和UI线程在同一个线程。否则就会出现,UI线程正在绘制页面,而另外能操作UI的线程对View进行了操作,当UI线程绘制完上方的View后,那么这个被其他线程操作后的VIew的很有可能会覆盖到其他View之上,这并不是我们想看到的结果。

最后

“Android UI操作并不是线程安全的”这句话,个人理解是如果ViewRootImpl不强制检查线程,那么,任何都可以更改View的属性,无法保证同一帧数据的完整性。 或许控制View绘制的线程和通知View更新的线程必须是同一线程,比主线程更新UI更能表达出这层一次吧。

参考资料:
https://www.cnblogs.com/dasusu/p/8311324.html
https://blog.csdn.net/aigestudio/article/details/43449123

Copyright© 2013-2019

京ICP备2023019179号-2