【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@Overridepublic ViewParent invalidateChildInParent(int[] location, Rect dirty) {checkThread();//省略无关代码}@Overridepublic 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打印233binding.demo1.text="NEW"}
}

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

【Kotlin】协程的字节码原理

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

  lifecycleScope.launchWhenResumed {withContext(Dispatchers.IO) {Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233Thread.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.javaprivate void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {//省略无关代码if (mLayout != null) {checkForRelayout();}    //省略无关代码           }

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

//TextView.javaprivate 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.javaif (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 pathonDescendantInvalidated(child, child);return;}//省略无关代码}

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

//ViewRootImpl.java@Overridepublic 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();}@UnsupportedAppUsagevoid 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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NumPy 中数组拼接、合并详解

1、np.append() 1.1、语法 将值添加到数组的末端,返回一个新的数组,而原数组不变。 numpy.append(arr, values, axisNone)参数描述arr : 类数组输入的数组values : 类数组向数组 arr 添加的元素,需要与 arr 维度相同axis : 整型添加操作的…

python对自动驾驶进行模拟

使用了 Pygame 库来创建一个简单的游戏环境,模拟了一辆自动驾驶汽车在道路上行驶。汽车的位置和速度通过键盘控制,可以左右移动和加速减速。道路的宽度和颜色可以根据需要进行调整。 import pygame import random # 游戏窗口大小 WINDOW_WIDTH 800 WINDOW_HEIG…

git 删除 submodule 子模块的步骤

实验有效,这里删除了两个 submodule。 1, 执行删除 submodule mkdir tmp1 && cd tmp1 && git clone --recursive ssh://gitaaa.bbb.ccc.git \ && cd ccc/ && git checkout -b abranch_01 \ && git submodule deinit -f…

我终于学会的前端技能——代码调试、打断点

在技术的世界里,要用魔法来打败魔法 说来惭愧我做前端已近三年了竟然还没有学会如何调试代码,也就是给自己的代码打上断点一步步看它的运行状态以达到理清代码运行逻辑、排查问题提升开发效率的目的。直到最近我才学会了这一技能,在这之前我…

Neos的渗透测试靶机练习——DarkHole-1

DarkHole-1 一、实验环境二、开始渗透1. 搜集信息2. sql注入4. 提权 三、总结 一、实验环境 虚拟机软件:VirtualBox 攻击机:kali linux(网卡初始为仅主机模式,要有安全意识) 靶机:DarkHole-1(网…

磁盘位置不可用怎么修复?

磁盘位置不可用是计算机使用中经常遇到的问题。造成磁盘位置不可用的原因有多种,其中最常见的是磁盘文件系统损坏。当文件系统损坏时,操作系统无法正常访问磁盘上的数据,导致磁盘位置不可用。 磁盘位置不可用怎么修复? 当磁盘位置…

2024年北京市安全员-C3证证模拟考试题库及北京市安全员-C3证理论考试试题

题库来源:安全生产模拟考试一点通公众号小程序 2024年北京市安全员-C3证证模拟考试题库及北京市安全员-C3证理论考试试题是由安全生产模拟考试一点通提供,北京市安全员-C3证证模拟考试题库是根据北京市安全员-C3证最新版教材,北京市安全员-C…

探索数据之美:深入Seaborn的数据可视化艺术与技巧【第26篇—python:Seaborn】

文章目录 1. 引言2. Seaborn基础2.1 安装和环境设置2.2 常用数据可视化函数2.3 设置样式和颜色主题 3. 数据准备与导入3.1 使用Pandas库加载和处理数据3.2 数据清理和缺失值处理 4. Seaborn中的常见图表4.1 折线图和散点图:展示趋势和变量关系4.2 条形图和箱线图&am…

个性化定制的知识付费小程序,为用户提供个性化的知识服务

明理信息科技知识付费saas租户平台 随着知识经济的兴起,越来越多的人开始重视知识付费,并希望通过打造自己的知识付费平台来实现自己的知识变现。本文将介绍如何打造自己的知识付费平台,并从定位、内容制作、渠道推广、运营维护四个方面进行…

Spring Boot 优雅实现统一数据返回格式+统一异常处理+统一日志处理

在我们的项目开发中,我们都会对数据返回格式进行统一的处理,这样可以方便前端人员取数据,当然除了正常流程的数据返回格式需要统一以外,我们也需要对异常的情况进行统一的处理,以及项目必备的日志。 1. 统一返回格式 …

归并排序(C语言)

目录 1.归并排序图解 2.归并排序(递归版) 3.归并排序(非递归版) 1.归并排序图解 归并排序的核心思想是让左右两边有序的部分进行合并比较排序,具体什么意思呢?分两点: 1.分:左右两边…

【数据结构】排序之归并排序与计数排序

个人主页 : zxctsclrjjjcph 文章封面来自:艺术家–贤海林 如有转载请先通知 目录 1. 前言2. 归并排序2.1 递归实现2.1.1 分析2.1.2 代码实现 2.2 非递归实现2.2.1 分析2.2.2 代码实现 3. 计数排序3.1 分析3.2 代码实现 4. 附代码4.1 Sort.h4.2 Sort.c4.3…