Android 子线程为什么不能更新UI?

news/2024/7/7 5:03:53 标签: android, 线程, handler

Android 应用的 UI 是在主线程上进行绘制和更新的。

当我们在子线程中直接进行 UI 更新时,会导致以下问题:

1. 线程安全问题:多个线程同时操作 UI,可能导致 UI 组件的状态不一致或者出现竞争条件。
2. 卡顿和 ANR:如果在主线程中执行耗时操作,会导致主线程被阻塞,用户界面无法响应用户的输入,甚至可能发生 ANR(Application Not Responding)错误。

 移步:子线程怎么切换主线程? 

(一)现象
在子线程中直接更新UI就会crash,报错如下:
android.view.ViewRootImpl$CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.
只有创建了视图层次结构的原始线程才能访问它的视图。
也就是说,ui在哪个线程创建的,就应该在哪个线程中更新。而UI是在主线程中创建的,所以就应该在主线程中更新UI。这是crash的信息提示。

(二)原因
首先要明白UI更新的原理,就是通过不断的测量、布局和绘制的过程。最终都会请求view的绘制操作,即requestLayout()这个方法。

View.java:
public void requestLayout() {
//当父视图不为空且有请求布局时,执行父视图的requestLayout()方法
if (mParent!=null && !mParent.isLayoutRequested()){
mParent.requestLayout();
}
}
protected ViewParent mParent; //当前视图的父视图
//父视图的赋值方法
void assignParent(ViewParent parent) {
if (mParentnull) {
mParent = parent;
} else if (parentnull) {
mParent = null;
} else {
throw new RuntimeException(“view " + this + " being added, but it already has a parent”);
}
}


从源码可以看出,当请求布局时,会调用父视图mParent的requestLayout()方法,mParent是一个接口对象,requestLayout()方法是接口方法。所以需要找到实现这个接口的类。
可以发现,在assignParent(ViewParent parent)方法中对mParent进行了赋值,那么assignParent(ViewParent parent)在哪里被调用的呢?
通过阅读Activity的启动过程源码,可以发现:

(1)在Activity的onCreate()方法中调用了setContentView()方法,其实最终调用了顶层视图PhoneWindow的setContentView()方法,然后调用其内部的installDecor()初始化mDecor;
(2)紧跟着在handleLaunchActivity()中调用了handleResumeActivity();
(3)然后在handleResumeActivity()中可以发现调用了WindowManagerImpl的addView方法,wm.addView(decor, l)
(4)然后又调用了WindowManagerGlobal类的addView()方法,在里面可以发现又调用了root.setView(view, wparams, panelParentView),这个root是ViewRootImpl一个实例。
然后直接看ViewRootImpl的setView()方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//…
view.assignParent(this);
//…
}
}
}

由此可见,最终在ViewRootImpl的setView()方法中对mParent进行了赋值。同时也指明了mParent是ViewRootImpl的一个实例,实现了ViewParent接口,所以很显然了,前面提到的调用了mParent 的requestLayout()方法,即是ViewRootImpl的requestLayout()。

ViewRootImpl.java:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) { // 1.请求更新布局
checkThread(); // 2.首先检查当前所在线程
//…
}
}
void checkThread() {
// 3.直接判断view所属的线程是否为当前线程,否则抛出异常
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadExcept ion( “Only the original thread that created a view hierarchy can touch its views.”);
}
}

final Thread mThread; //存放view所属的线程
所以,在进行UI更新时,都会进行线程的检测判断。以上就是为什么不能在子线程中直接更新UI的原因原理。
为了避免上述问题,Android 引入了一种机制,允许我们在子线程中将任务切换到主线程执行。

移步:子线程怎么切换主线程
 


http://www.niftyadmin.cn/n/5427366.html

相关文章

ubuntu如何添加快捷方式到收藏夹、桌面

一、背景 有时候单独下载的软件包需要在特定路径里启动,这样使用起来非常不方便。因此需要在桌面和收藏夹里创建启动快捷方式。 二、具体步骤 这里以下载的zotero软件(一款用于文献管理的软件)为例。官网地址: Zotero | Your personal res…

程序员应知必会的 6 种常见数据模型

本文转自 公众号 ByteByteGo,如有侵权,请联系,立即删除 程序员应知必会的 6 种常见数据模型 今天来聊聊常见的 6 大数据模型. 数据模型为数据库管理系统(DBMS)中的数据存储、检索和操作提供了基础,并影响…

Parade Series - WebRTC ( < 300 ms Low Latency ) T.B.D

Parade Series - FFMPEG (Stable X64) 延时测试秒表计时器 ini/config.ini [system] homeserver storestore\nvr.db versionV20240312001 verbosefalse [monitor] listrtsp00,rtsp01,rtsp02 timeout30000 [rtsp00] typelocal deviceSurface Camera Front schemartsp ip127…

代码随想录算法训练营第day44|完全背包、518. 零钱兑换 II 、377. 组合总和 Ⅳ

目录 完全背包 518.零钱兑换II 377. 组合总和 Ⅳ 完全背包 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包…

Lua 如何读写ini文件

常见的配置文件通常使用ini文件来存储,读写ini文件的方式也有很多。本文想要实现的是:ini文件的读写由Lua实现,C只负责调用Lua来实现ini文件的读写功能。那么如何在C代码中调用Lua来实现ini文件的读写? ini.lua --LoadIniFile f…

Unity3d版白银城地图

将老外之前拼接的Unity3d版白银城地图,导入到国内某手游里,改成它的客户端地图模式,可以体验一把手游的快乐。 人物角色用的是它原版的手游默认的,城内显示效果很好,大家可以仔细看看。 由于前期在导入时遇到重大挫折&…

Cap2:Pytorch转TensorRT(上:Pytorch->ONNX)

文章目录 1、pytorch导出onnx模型2、使用onnxruntime推理onnx模型3、精度对齐4、总结 深度学习框架种类繁多,想实现任意框架之间的模型转换是一件困难的事情。但现在有一个中间格式ONNX,任何框架模型都支持转为ONNX,然后也支持从ONNX转为自身…

IBatis与MyBatis区别

在sqlMap里面&#xff0c;iBatis的传入参数是parameterClass&#xff0c;而MyBatis是可以不写的&#xff0c;也可以用parameterType&#xff0c;iBatis的传出参数是resultClass。 iBatis&#xff1a; <select id"selectDeviceByWhere" parameterClass"Map&q…