当前位置:   article > 正文

Kotlin协程之Dispatchers.IO番外篇-更新UI_lifecyclescope.launch(dispatchers.io)

lifecyclescope.launch(dispatchers.io)

协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

回顾下:Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:

  • Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数,运行 Android 界面框架操作,以及更新LiveData对象。
  • Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用Room组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
  • Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。

对于Dispatchers.IO 以下调用会报错:

  1. lifecycleScope.launch(Dispatchers.IO) {
  2. // 这里报错:Only the original thread that created a view hierarchy can touch its views.
  3. testView.text = "测试"
  4. }
  1. at android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
  2. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
  3. at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)

因为这在IO线程中来更新UI了。

### 那么下面来看代码示例-1

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. ...
  4. lifecycleScope.launch(Dispatchers.IO) {
  5. testView.text = "测试"
  6. }
  7. ...
  8. }

示例-1 代码中也更新UI,而它不会报错:Only the original thread that created a view hierarchy can touch its views.
这就很奇怪了,这里也在IO线程中来更新了UI! 为什么呢?

#1. 开始追逐源码:根据报错提示,发现报错地方是:ViewRootImpl.checkThread(),我们可以理解为 示例-1 没有执行该方法:

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

继续根据报错提示,会发现是这个方法: ViewRootImpl.requestLayout()

  1. /**
  2. * The top of a view hierarchy, implementing the needed protocol between View
  3. * and the WindowManager. This is for the most part an internal implementation
  4. * detail of {@link WindowManagerGlobal}.
  5. *
  6. * {@hide}
  7. */
  8. @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
  9. public final class ViewRootImpl implements ViewParent,
  10. View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
  11. ...
  12. @Override
  13. public void requestLayout() {
  14. if (!mHandlingLayoutInLayoutRequest) {
  15. checkThread();
  16. mLayoutRequested = true;
  17. scheduleTraversals();
  18. }
  19. }
  20. ...
  21. }

里面有个参数:mHandlingLayoutInLayoutRequest,如果

mHandlingLayoutInLayoutRequest = true

就不会调用 checkThread(),我们离真相更近一步。当为True时的条件是:

  1. // Set this flag to indicate that any further requests are happening during
  2. // the second pass, which may result in posting those requests to the next
  3. // frame instead
  4. mHandlingLayoutInLayoutRequest = true;

注释的意思是:设置此标志以指示在第二次传递期间正在发生任何进一步的请求,这可能导致将这些请求发布到下一帧,很难懂!!!

#2. 摸不着头脑,那我们换个思路来,从 testView.text = "测试" 这行代码来查看 TextView.java

  1. // TextView.java
  2. @android.view.RemotableViewMethod
  3. public final void setText(CharSequence text) {
  4. setText(text, mBufferType);
  5. }
  6. public void setText(CharSequence text, BufferType type) {
  7. setText(text, type, true, 0);
  8. ...
  9. }
  1. @UnsupportedAppUsage
  2. private void setText(CharSequence text, BufferType type,
  3. boolean notifyBefore, int oldlen) {
  4. ...
  5. if (mLayout != null) {
  6. checkForRelayout();
  7. }
  8. ...
  9. }

这里我们看下 checkForRelayout() 

  1. // TextView.java
  2. @UnsupportedAppUsage
  3. private void checkForRelayout() {
  4. // If we have a fixed width, we can just swap in a new text layout
  5. // if the text height stays the same or if the view height is fixed.
  6. if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
  7. || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
  8. && (mHint == null || mHintLayout != null)
  9. && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
  10. ...
  11. // We lose: the height has changed and we have a dynamic height.
  12. // Request a new view layout using our new text layout.
  13. requestLayout();
  14. invalidate();
  15. } else {
  16. // Dynamic width, so we have no choice but to request a new
  17. // view layout with a new text layout.
  18. nullLayouts();
  19. requestLayout();
  20. invalidate();
  21. }
  22. }

可以看到始终会执行方法 requestLayout() ,但是这里是View.java类中,进一步查看:

  1. // View.java
  2. @CallSuper
  3. public void requestLayout() {
  4. ...
  5. if (mParent != null && !mParent.isLayoutRequested()) {
  6. mParent.requestLayout();
  7. }
  8. ...
  9. }

现在可以看到 mParent.requestLayout() ,这里的 mParent 是 ViewParent 的对象,而ViewRootImpl类是ViewParent的子类,上面的代码首先判断mParent是否为空,也就是ViewRootImpl对象,可以想象这个时候是因为 ViewRootImp 还没有创建,所以不会执行线程检查;而ViewRootImpl对象是在onResume()方法之后才创建的。
有兴趣的可以去查看下去寻找ViewRootImp什么时候创建,从Activity启动时查找源代码,通过分析可以查看ActivityThread.java源代码。

因此可以解答了前面的问题:在示例-1中启动IO线程来更新UI,是可以正常更新UI的。

而且我们在 onStart()和onResume()方法中启动子线程来更新UI都不会报错。

后语:要更新UI一定要在主线程中实现

谷歌提出:“一定要在主线程中UI”,其实是为了提高界面的效率和安全性,带来更好的流畅度;退一步,如果允许多线程更新UI,但是访问UI没有被锁定,一旦多线程抢占资源,那么界面就会无序更新,体验效果不言而喻;所以在Android必须在主线程中更新UI

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/495708
推荐阅读
相关标签
  

闽ICP备14008679号