为什么子线程不能更新UI
不同场景验证
不少同学在开发时都碰到过如下报错
AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. Expected: main Calling: Thread-1
一查代码,原来自己在子线程中去更新UI了界面。比如,我要按下一个Button时,我需要更新button中的text,代码很简单:
Button button = findViewById(R.id.button);
button.setOnClickListener(v -> new Thread(new Runnable() {
@Override
public void run() {
button.setText("update text in a new thread");
}
}).start());
就会有如下报错:
AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. Expected: main Calling: Thread-1 AndroidRuntime: at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9994) AndroidRuntime: at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:2082) AndroidRuntime: at android.view.View.requestLayout(View.java:27043) ...
但是细心的同学又会发现,有时候在子线程中更新UI又没有问题,这是怎么一回事?来验证下。
package com.zte.oomsimulator;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
Button button = findViewById(R.id.button);
button.setText("update text in a new thread --- onCreate");
Log.d(TAG, "run: onCreate, setText successfully");
}
}).start();
Button button = findViewById(R.id.button);
button.setOnClickListener(v -> new Thread(new Runnable() {
@Override
public void run() {
button.setText("update text in a new thread when button was clicked");
Log.d(TAG, "run: onClick, setText successfully");
}
}).start());
}
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
Button button = findViewById(R.id.button);
button.setText("update text in a new thread --- onResume");
Log.d(TAG, "run: onResume, setText successfully");
}
}).start();
}
}
按照这个写法,实测onCreate和onResume中的更新UI动作不会有问题,只有在Button中开启的子线程更新UI会报错。问题出在哪里?
子线程更新UI报错
根据报错的堆栈我们可以追溯到,报错的地方是在ViewRootImpl.checkThread, 来看下源代码
// framework/base/core/java/android/view/ViewRootImpl.java
void checkThread() {
Thread current = Thread.currentThread();
if (mThread != current) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views."
+ " Expected: " + mThread.getName()
+ " Calling: " + current.getName());
}
}
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
WindowLayout windowLayout) {
mContext = context;
mWindowSession = session;
mWindowLayout = windowLayout;
mDisplay = display;
mBasePackageName = context.getBasePackageName();
mThread = Thread.currentThread(); // mThread在ViewRootImpl构造函数中就被赋值了。
...
checkThread检验当前线程否和mThread一致。如果不一致则会抛出CalledFromWrongThreadException错误。 mThread是什么?在ViewRootImpl构造函数中就已经被赋值了。因此只需要确认在哪里创建的ViewRootImpl即可。
直接在工程中查找 new ViewRootImpl,WindowManagerGlobal#addView中有新建ViewRootImpl,
// core/java/android/view/WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
...
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display,
windowlessSession, new WindowlessWindowLayout());
}
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
...
}
那么是哪里调用到了WindowManagerGlobal#addView?其实如果熟悉Activity启动流程很容易追溯,在ActivityThread.handleResumeActivity中调用了WindowManagerGlobal#addView,而且 ActivityThread 所在线程正是应用主线程。
综上所述,即可得到在子线程中不可更新UI的结论。
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
...
// TODO Push resumeArgs into the activity for consideration
// skip below steps for double-resume and r.mFinish = true case.
if (!performResumeActivity(r, finalStateRequest, reason)) { // performResumeActivity -> xxx -> Activity.onResume
return;
}
...
...
if (r.window == null && !a.mFinished && willBeVisible) {
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l); // WindowManagerGlobal#addView
...
}
...
}
...
}
onResume中子线程更新UI为什么没问题
重新回到ViewRootImpl,理论上只要触发了checkThread,那么就会检验线程的一致性。难道在onResume中的线程操作没有触发checkTread?重新追溯一遍源码看看。
ViewRootImpl.checkThread是由View.requestLayout调用,果然是有条件的,需要满足 mParent != null && !mParent.isLayoutRequested()
// core/java/android/view/View.java
@CallSuper
public void requestLayout() {
if (isRelayoutTracingEnabled()) {
Trace.instantForTrack(TRACE_TAG_APP, "requestLayoutTracing",
mTracingStrings.classSimpleName);
printStackStrace(mTracingStrings.requestLayoutStacktracePrefix);
}
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout(); // ViewRootImpl.requestLayout -> ViewRootImpl.checkThread
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
继续查看View的源代码发现,mParent赋值是在assignParent进行的:
// core/java/android/view/View.java
@UnsupportedAppUsage
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
那是在哪里调用了View#assignParent?在上面 WindowManagerGlobal#addView的源码中,最后调用了root.setView,来看下ViewRootImpl.setView这部分代码:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
...
view.assignParent(this); // 设置View中的mParent为ViewRootImpl实例
mAddedTouchMode = (res & WindowManagerGlobal.ADD_FLAG_IN_TOUCH_MODE) != 0;
mAppVisible = (res & WindowManagerGlobal.ADD_FLAG_APP_VISIBLE) != 0;
...
}
}
}
巧了,正是在此设置了View的mParent。到这里基本上就能解释为什么在onResume中开子线程更新UI不会有任何问题:因为在生命周期执行到onResume时,此时的mParent为空,不会调用到checkThread做线程一致性校验,所以可以开子线程更新UI。
为什么要设计成这种只能在主线程更新UI的模式?
首先,个人认为要从技术角度实现多线程更新UI也是完全可以实现的,但可能有如下弊端:
- 会引起并发冲突,Android UI刷新是一个高频操作,如果允许多个线程直接操作 UI,可能会导致竞态条件或者不一致的状态,结果无法预测。解决办法只能做同步处理,若强制要求所有 UI 操作加锁(如
synchronized),虽然能解决线程安全问题,但会增加代码复杂度和性能开销(锁竞争、死锁风险),同步成本太高了。 - 保证事件的一致性:
- 用户事件需要按序处理:用户的点击、滑动等输入事件是按顺序发生的。主线程通过 消息队列(MessageQueue) 机制(如
Looper、Handler)确保这些事件按 FIFO(先进先出)顺序处理。 - 避免界面状态混乱:如果允许其他线程直接更新 UI,可能打断主线程正在处理的逻辑(例如:后台线程突然修改某个控件的尺寸,而主线程正在计算布局),导致界面渲染错误。
- 用户事件需要按序处理:用户的点击、滑动等输入事件是按顺序发生的。主线程通过 消息队列(MessageQueue) 机制(如
- 性能优化:集中化 UI 渲染
- 渲染管线单线程化:Android 的界面渲染流程(测量、布局、绘制)本身是单线程的。系统需要在一个统一的上下文中(如
Choreographer)协调这些操作,以保证渲染效率。 - 减少多线程切换开销:频繁的线程切换(Context Switching)会浪费 CPU 资源,而主线程集中处理 UI 更新可以降低这种开销。
- 渲染管线单线程化:Android 的界面渲染流程(测量、布局、绘制)本身是单线程的。系统需要在一个统一的上下文中(如