Kotlin协程之Dispatchers.IO番外篇-更新UI

Kotlin协程之Dispatchers.IO番外篇-更新UI,第1张

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

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

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

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

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

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

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

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ...
        
        lifecycleScope.launch(Dispatchers.IO) {
            testView.text = "测试"
        }
     
        ...
    }

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

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

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

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

/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {

    ...

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

    ...

}

里面有个参数:mHandlingLayoutInLayoutRequest,如果

mHandlingLayoutInLayoutRequest = true

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

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

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

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

    // TextView.java
    @android.view.RemotableViewMethod
    public final void setText(CharSequence text) {
        setText(text, mBufferType);
    }

    public void setText(CharSequence text, BufferType type) {
        setText(text, type, true, 0);

        ...
    }
    @UnsupportedAppUsage
    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        ...
   
        if (mLayout != null) {
            checkForRelayout();
        }

        ...
    }

这里我们看下 checkForRelayout() 

    // TextView.java
    @UnsupportedAppUsage
    private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.

        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {

            ...

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

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

    // View.java
    @CallSuper
    public void requestLayout() {

        ...

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }

        ...
    }

现在可以看到 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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/992554.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存