【Android】为什么在子线程中更新UI不会抛出异常

转载请注明来源:https://blog.csdn.net/devnn/article/details/135638486

前言

众所周知,Android App在子线程中是不允许更新UI的,否则会抛出异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

详细异常信息见下图
在这里插入图片描述
View的绘制是在ViewRootImpl中(关于view的绘制流程不是本文重点):

//ViewRootImpl.java
 @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        //省略无关代码
     }

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

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

问题

笔者偶然发现在协程中是可以更新UI的,比如在Activity的onCreate有以下一段代码:

  lifecycleScope.launchWhenResumed {
        withContext(Dispatchers.IO) {
        Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233
        binding.demo1.text="NEW"
 	}
}

这其实已经是在子线程中更新UI,为什么不会抛出异常呢?难道是协程检测到是UI操作自动帮我们切换到了主线程?经过笔者上一篇文章对协程的字节码分析,排除了这种可能。

【Kotlin】协程的字节码原理

难道是页面还没有开始绘制,还没有调用ViewRootImpl.checkThread()代码吗?那让子线程更新UI操作之前先休眠等待一段时间呢?

  lifecycleScope.launchWhenResumed {
        withContext(Dispatchers.IO) {
        Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233
        Thread.sleep(10000)
        binding.demo1.text="NEW"
 	}
}

经验证也是没有抛出异常。这就有点匪夷所思了!

另外,改用直接使用Thread创建子线程也是同样不会抛异常:

 Thread {
     Thread.sleep(10000)
     val button1 = binding.demo1.text="NEW"
 }.start()

这也验证了跟协程是没有关系的。

那只有从源码中寻找答案。

setText流程

看看TextViewsetText方法的源码。
public void setText(CharSequence text)方法内部会调到以下4个参数的重载方法。

//TextView.java
  private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
       //省略无关代码
      if (mLayout != null) {
            checkForRelayout();
       }    
      //省略无关代码           
   }

checkForRelayout方法用来判断是调用invalidate还是requestLayout来更新UI。

//TextView.java
 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)) {
            // Static width, so try making a new text layout.

            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // 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();
        }
    }

查看checkForRelayout方法会发现,当TextView的宽高是写死的,或者宽高跟之前没有变化,那么就调invalidate(),否则调用requestLayout。

笔者经过断点验证,发现在协程中调用的setText方法内部走到以下if语句内部然后return了,这说明宽高没有变化,调用了invalidate()方法来更新UI。

//TextView.java
    if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }

invalildate方法会调用到parent的invalidateChild方法:

//ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            // HW accelerated fast path
            onDescendantInvalidated(child, child);
            return;
        }
        //省略无关代码
    }

可以发现,当attachInfo非空并且开启了硬件加速,那么就走onDescendantInvalidated流程。View的onDescendantInvalidated方法最终会递归到ViewRootImplonDescendantInvalidated方法:

//ViewRootImpl.java
   @Override
    public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
        // TODO: Re-enable after camera is fixed or consider targetSdk checking this
        // checkThread();
        if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            mIsAnimating = true;
        }
        invalidate();
    }

    @UnsupportedAppUsage
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }

ViewRootImpl的onDescendantInvalidated方法直接调用了invalidate并没有调用checkThread方法。

硬件加速默认是开启了,可以使用view的isHardwareAccelerated方法判断是否开启:

 lifecycleScope.launchWhenResumed {
 		withContext(Dispatchers.IO) {
 		Thread.sleep(10000)
		binding.demo1.text="NEW"
		Log.i("MainActivity", "demo1.isHardwareAccelerated:${binding.demo1.isHardwareAccelerated}")
	}
}

当给Application配置关闭硬件加速后: android:hardwareAccelerated="false"
以上代码正如所料抛出了异常。

结论

经过以上分析,当使用invalidate更新UI并且开启了硬件加速,那么是可以在子线程中更新UI的。

还有一种情况就是DecorView还没有添加到Window中(相当于ViewTree还没有渲染)的情况下,在子线程中也是可以更新UI的,但是更新不会立即生效,因为这个时候ViewRootImpl还没有创建,比如在onCreate中开启子线程立即更新UI。

转载请注明来源:https://blog.csdn.net/devnn/article/details/135638486

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/330415.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

芯片新闻-Global Semiconductor Sales Increase 5.3% Year-to-Year in November

11 月标志着一年多以来市场同比增长的第一个月;全球芯片销量环比增长2.9% 华盛顿——一月。 2024 年 12 月 9 日——半导体行业协会 (SIA) 今天宣布,2023 年 11 月全球半导体行业销售额总计 480 亿美元,比 2022 年 11 月的 456 亿美元总额增…

rust获取本地外网ip地址的方法

大家好,我是get_local_info作者带剑书生,这里用一篇文章讲解get_local_info的使用。 get_local_info是什么? get_local_info是一个获取linux系统信息的rust三方库,并提供一些常用功能,目前版本0.2.4。详细介绍地址&a…

大屏数据可视化的设计流程及原则

随着数字经济的快速发展和信息化在各行业各领域的深入推进,可视化大屏在各行各业得到越来越广泛的应用。可视化大屏不再只是电影里奇幻的画面,而是被实实在在地应用在政府、商业、金融、制造、交通、城市等各个行业的业务场景中,切切实实地实…

「alias」Linux 给命令起别名,自定义bash命令

0. 背景 Arch 系统没有 ll命令,在其他发行版用惯了一时间没有真不习惯,来配置一下吧! 1. 全局配置 我希望 ll 命令可以被所有人使用,所以应该配置在全局的bash配置文件中,一般这个全局bash配置文件在: /etc/bash.bashrc 切好管理员权限后,命令如下 echo “alias ll‘ls -l -…

React的合成事件

合成事件:通过事件委托,利用事件传播机制,当事件传播到document时,再进行分发到对应的组件,从而触发对应所绑定的事件,然后事件开始在组件树DOM中走捕获冒泡流程。 原生事件 —— > React事件 —— >…

TMDB电影数据分析(下)

TMDB电影数据分析(下) 本文对源自Kaggle TMDB电影数据集进行分析影响电影票房的因素,数据分析流程包含数据集概分析、数据清洗、数据统计以及分析影响电影票房的因素。影响票房因素可能是电影预算、电影类型、电影时长、受欢迎程度、电影评分…

十二、Qt 操作PDF文件(2)

一、在《十、Qt 操作PDF文件-CSDN博客》中我们用Poppler类库打开了PDF文件,并显示到窗体上,但只能显示一页,功能还没完善,在本章节中,加入了: 通过选择框选择PDF文件并打开,默认打开第一页。通…

【机器学习300问】11、多元线性回归模型和一元线性回归有什么不同?

在之前的文章中,我们已经学习了一元线性回归模型,其中最关键的参数是w和b。机器学习的目的就是去得到合适w和b后能准确预测未知数据。但现实世界是复杂的,一个事情的发生绝大多数时候不会是一个原因导致。 因此多元线性回归模型区别与一元线性…

虚幻UE 特效-Niagara特效实战-火焰、烛火

在上一篇笔记中:虚幻UE 特效-Niagara特效实战-烟雾、喷泉 我们进行了烟雾和喷泉的实战,而今天这篇笔记 我们在不使用模板的前提下对火焰和烛火特效进行实战 文章目录 一、火焰1、创建火焰的Niagara系统2、分析火焰是怎样的特征3、优化设置 二、烛火1、创…

cefsharp120.2.50(cef120.2.5,Chromium6167)升级测试及其他H264版本

一、版本变化 1.1 本次版本 本版本暂不支持H264,请参考其他版本,见文章底部。 有关cefsharp更新说明,这几天github打不开就不截图了。较上一版本没有大的变更。 1.2 H264版本 推荐版本:V100,V109,V111,V119版本 二、升级过程及注意事项 三、相关版本(H264版本) 私信…

RT-Thread Studio学习(十四)ADC

RT-Thread Studio学习(十四)ADC 一、简介二、新建RT-Thread项目并使用外部时钟三、启用ADC四、测试 一、简介 本文将基于STM32F407VET芯片介绍如何在RT-Thread Studio开发环境下使用ADC设备。硬件及开发环境如下: OS WIN10STM32F407VET6STM…

2.常见的点云数据滤波的方法总结(C++)

常见的点云数据处理有体素网格滤波、半径滤波、直通滤波、双边滤波器,统计滤波器,卷积滤波,条件滤波,高斯滤波等等。每种方法的原理和代码如下: 1.体素网格滤波 体素网格滤波是对密度大的三维的点在保持原来形状的条件…

Go后端开发 -- 反射reflect

Go后端开发 – 反射reflect && 结构体标签 文章目录 Go后端开发 -- 反射reflect && 结构体标签一、反射reflect1.编程语言中反射的概念2.interface 和反射3.变量内置的pair结构4.reflect的基本功能TypeOf和ValueOf5.从relfect.Value中获取接口interface的信息6…

SSL证书自动化管理有什么好处?如何实现SSL证书自动化?

SSL证书是用于加密网站与用户之间传输数据的关键元素,在维护网络安全方面,管理SSL证书与部署SSL证书一样重要。定期更新、监测和更换SSL证书,可以确保网站的安全性和合规性。而自动化管理可以为此节省时间,并避免人为错误和不必要…

React 基于Ant Degisn 实现table表格列表拖拽排序

效果图: 代码: myRow.js import { MenuOutlined } from ant-design/icons; import { DndContext } from dnd-kit/core; import { restrictToVerticalAxis } from dnd-kit/modifiers; import {arrayMove,SortableContext,useSortable,verticalListSorti…

【Java实战项目】基于ssm的数据结构课程网络学习平台

🙊作者简介:多年一线开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板&#xff…

Datawhale 强化学习笔记(二)马尔可夫过程,DQN 算法

文章目录 参考马尔可夫过程DQN 算法(Deep Q-Network)如何用神经网络来近似 Q 函数如何用梯度下降的方式更新网络参数强化学习 vs 深度学习 提高训练稳定性的技巧经验回放目标网络 代码实战 DQN 算法进阶Double DQNDueling DQN 算法代码实战 参考 在线阅…

计算机网络-计算机网络的概念 功能 发展阶段 组成 分类

文章目录 计算机网络的概念 功能 发展阶段总览计算机网络的概念计算机网络的功能计算机网络的发展计算机网络的发展-第一阶段计算机网络的发展-第二阶段-第三阶段计算机网络的发展-第三阶段-多层次ISP结构 小结 计算机网络的组成与分类计算机网络的组成计算机网络的分类小结 计…

RDMA原理浅析

1. DMA和RDMA概念 1.1 DMA DMA(直接内存访问)是一种能力,允许在计算机主板上的设备直接把数据发送到内存中去,数据搬运不需要CPU的参与。 传统内存访问需要通过CPU进行数据copy来移动数据,通过CPU将内存中的Buffer1移动到Buffer2中。DMA模式…

【Axure高保真原型】文字翻页效果

今天和大家分享选择文字翻页效果的原型模板,我们通过这个模板实现类似翻书的效果。鼠标点击右箭头,可以翻开下一页,点击左箭头翻开上一页;当然我们也可以通过鼠标拖动的操作进行翻页,鼠标想左拖动时,翻开下…