首先,我们应该谨记
Android 默认不允许在子线程中直接更新 UI
但是,有时候写代码没注意,在子线程更新了UI,一切表现正常也没有报错,这是为什么?
1.为什么不允许子线程更新UI
这是因为 Android 的 UI 控件(如 TextView、Button 等)并不是线程安全的,Android 系统设计了单线程模型(Single Thread Model)
如果直接在子线程更新 UI,系统会抛出 CalledFromWrongThreadException 异常,提示 “Only the original thread that created a view hierarchy can touch its views”
2.间接的子线程更新 UI 方式
虽然不能直接更新,但 Android 提供了多种安全的方式,让子线程通过主线程更新 UI(最终还是通过主线程更新的UI,只是提供了快捷的方式让子线程的UI更新任务放到主线程去实现),以下是最常用的几种:
2.1 方式 1:使用 runOnUiThread(Activity 专属)
这是最简单的方式,Activity 自带的方法,内部会判断当前线程是否为主线程,若非主线程则将任务 post 到主线程执行。
// 在Activity中
new Thread(new Runnable() {
@Override
public void run() {
// 子线程执行耗时操作(如网络请求、数据计算)
String result = doTimeConsumingTask();
// 切换到主线程更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
// 这里可以安全更新UI
textView.setText(result);
progressBar.setVisibility(View.GONE);
}
});
}
}).start();
2.2 方式 2:使用 Handler(通用)
Handler 是 Android 线程通信的核心工具,可将子线程的消息 /post 任务发送到主线程的消息队列,由主线程处理。
// 1. 在主线程创建Handler(关联主线程的Looper)
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 主线程中处理消息,更新UI
if (msg.what == 1) {
String result = (String) msg.obj;
textView.setText(result);
}
}
};
// 2. 子线程中发送消息
new Thread(new Runnable() {
@Override
public void run() {
String result = doTimeConsumingTask();
// 构建消息并发送到主线程
Message msg = Message.obtain();
msg.what = 1; // 消息标识
msg.obj = result; // 传递数据
mMainHandler.sendMessage(msg);
// 也可以直接post Runnable(更简洁)
// mMainHandler.post(new Runnable() {
// @Override
// public void run() {
// textView.setText(result);
// }
// });
}
}).start();
2.3 方式 3:使用 View.post()(仅更新当前 View 时)
每个 View 都自带 post 方法,内部会自动将任务投递到主线程执行,无需额外创建 Handler。
new Thread(new Runnable() {
@Override
public void run() {
String result = doTimeConsumingTask();
// 直接通过View切换到主线程
textView.post(new Runnable() {
@Override
public void run() {
textView.setText(result);
}
});
}
}).start();
2.4 方式 4:使用 AsyncTask(已过时,但了解即可)
Android 提供的异步任务工具(API 30 + 标记为过时),内部封装了线程池和 Handler,简化子线程更新 UI 的流程,现在更推荐使用Coroutine(协程)或RxJava。
3.子线程更新UI
文章开头也提到了,有时候在子线程执行了更新UI的代码,运行时也没有报错
核心原因:异常检测不是 “全时段” 的
Android 并不是对所有非主线程的 UI 操作都做了严格校验,它的CalledFromWrongThreadException异常触发有前提条件—— 只有当 UI 操作触发了ViewRootImpl的线程检查时,才会报错。
简单来说:ViewRootImpl 是 UI 线程检查的 “守门员”,只有当 ViewRootImpl 已经创建完成后,更新 UI 才会触发线程校验;如果 ViewRootImpl 还没创建,或者某些操作没走 ViewRootImpl 的校验逻辑,就不会报错。
具体场景与原理
下面列举几种常见的 “非主线程更新 UI 不报错” 的场景,并解释背后的逻辑:
场景 1:ViewRootImpl 创建前更新 UI(最常见)
ViewRootImpl 是连接 View 树和 WindowManager 的核心类,它的performTraversals()方法会触发 measure/layout/draw 流程,而线程检查就封装在 ViewRootImpl 的checkThread()方法中。
- 时机:Activity 的onCreate()/onStart()/onResume()方法执行时,ViewRootImpl 还未完成初始化(通常在onResume()执行完后,系统才会创建 ViewRootImpl 并触发第一次 UI 绘制);
- 示例:在onCreate()中开子线程更新 TextView 的文字,不会报错:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.tv_test);
// 子线程更新UI,不会报错
new Thread(() -> {
tv.setText("子线程更新UI");
}).start();
}
- 原因:此时 ViewRootImpl 还没创建,checkThread()方法根本没被调用,自然不会检测到线程异常。但这种操作极其不安全—— 如果 ViewRootImpl 后续创建完成,再在子线程更新就会立刻报错。
场景 2:更新 UI 但未触发视图重绘
有些 UI 操作只是修改了 View 的属性,但没有触发requestLayout()(布局重绘)或invalidate()(绘制重绘),就不会走到 ViewRootImpl 的校验逻辑:
比如仅修改 TextView 的tag(非 UI 显示属性)、或修改 View 的内部变量但不影响界面显示,这类操作不会触发重绘,因此不会触发线程检查;
但如果修改的是text、background、visibility等会触发重绘的属性,且 ViewRootImpl 已创建,就一定会报错。
场景 3:特殊的 Window 类型(如 SurfaceView)
普通 View(TextView/Button 等)的绘制依赖主线程的 UI 队列,但SurfaceView有独立的绘制表面(Surface),它的绘制可以在子线程中进行(只要做好同步),不会触发线程检查;
同理,TextureView、自定义的 GLSurfaceView 等 “离屏绘制” 的控件,也允许在子线程处理绘制逻辑(但仍需遵循自身的线程安全规则)。
场景 4:单例 / 全局 View(无 Window 关联)
如果一个 View 没有被添加到任何 Window(比如只是 new 了一个 TextView 但没 setContentView / 添加到布局),它没有关联 ViewRootImpl,此时在子线程修改它的属性,也不会触发线程检查 —— 因为这个 View 根本没有 “被绘制到屏幕” 的机会,系统无需校验。
关键提醒:“不报错”≠“安全”
即使某些场景下子线程更新 UI 没报错,也绝对不推荐这么做:
- 非主线程更新 UI 可能导致 UI 状态不一致(比如子线程和主线程同时修改同一个 View 的属性);
- 可能引发偶发的 ANR、界面卡顿甚至崩溃(比如 ViewRootImpl 创建完成后,子线程刚好执行 UI 操作);
- 这种 “不报错” 是系统机制的 “漏洞”,而非设计允许,不同 Android 版本的校验逻辑可能变化,适配风险极高。
4.总结
报错的核心触发点:只有当 UI 操作触发ViewRootImpl.checkThread()时,才会检测线程并抛出异常,而checkThread()仅在 ViewRootImpl 创建后、视图重绘时执行;
不报错的场景:ViewRootImpl 创建前、未触发重绘的 UI 操作、特殊离屏绘制控件、无 Window 关联的 View;
开发原则:无论是否报错,都必须遵循 “主线程更新 UI” 的规则,通过Handler/runOnUiThread等方式切换线程,避免潜在的线程安全问题。