叮铃铃,叮铃铃,“今天没有BUG”小课堂打铃上课了,一位长相极其帅气的讲师进入教室,教室中间,路人甲、乙、丙三位同学头戴红领巾坐下,双手平放在桌上准备认真听课。
老师在黑板写下今天的问题:“更新UI的操作,一定要在 UI 线程中进行吗?不在 UI 线程可不可以?”
同学甲回答:众所周知,更新 UI 的操作一定要在 UI 线程进行,否则程序会崩溃。
老师点点头,在黑板上奋笔疾书,让同学们检查下面这段代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".uicrash.UICrashActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/vTvRandom"
android:layout_width="200dp"
android:layout_height="40dp"
android:text="随便一点文字"
android:layout_centerInParent="true"/>
</RelativeLayout>
class UICrashActivity : ViewBindingActivity<ActivityUICrashBinding>() {
override fun initWidget() {
super.initWidget()
title = javaClass.simpleName
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.vTvRandom.text = "在UI线程修改UI"
}
override fun getViewBinding(): ActivityUICrashBinding = ActivityUICrashBinding.inflate(layoutInflater)
}
运行这段代码会报错吗?
同学们:不会
老师又点点头,修改 onCreate() 中的代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.vTvRandom.setOnClickListener {
ThreadPool.runOnNonUIThread{
mBinding.vTvRandom.text = "在非UI线程修改UI"
}
}
}
在 button 被点击的时候,将修改文字的操作放到非 UI 线程中运行。
大家觉得会报错吗?
还是路人甲积极,回答道:easy,肯定报错,我就是这么错过来的。
没错,确实报错了,看看堆栈信息:
2022-05-08 14:12:46.352 14420-14471/com.example.essay E/AndroidRuntime: FATAL EXCEPTION: bible-pool-0
Process: com.example.essay, PID: 14420
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1163)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:5207)
at android.view.View.invalidateInternal(View.java:13715)
at android.view.View.invalidate(View.java:13679)
at android.view.View.invalidate(View.java:13663)
at android.widget.TextView.checkForRelayout(TextView.java:7354)
at android.widget.TextView.setText(TextView.java:4487)
at android.widget.TextView.setText(TextView.java:4344)
at android.widget.TextView.setText(TextView.java:4319)
at com.jamgu.home.uicrash.UICrashActivity$onCreate$1$1.run(UICrashActivity.kt:24)
at com.jamgu.common.thread.ThreadPool$RunnableJob.run(ThreadPool.java:252)
at com.jamgu.common.thread.ThreadPool$Worker.run(ThreadPool.java:314)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:760)
at com.jamgu.common.thread.PriorityThreadFactory$newThread$1.run(PriorityThreadFactory.kt:17)
这个报错相信大家都很熟悉,Only the original thread that created a view hierarchy can touch its views.
,我们没在正确的线程更新 UI 操作。
这里的 original thread 翻译过来是原始线程的意思,原始线程会和我们的主线程意思一样吗?咱们稍后再谈。先看下这个错误是从哪里抛出来的,从上面的堆栈来看,代码的执行的先后顺序是:
com.jamgu.home.uicrash.UICrashActivity$onCreate$1$1.run(UICrashActivity.kt:24)
android.widget.TextView.setText(TextView.java:4319)
android.widget.TextView.setText(TextView.java:4344)
android.widget.TextView.setText(TextView.java:4487)
android.widget.TextView.checkForRelayout(TextView.java:7354)
android.view.View.invalidate(View.java:13663)
android.view.View.invalidate(View.java:13679)
android.view.View.invalidateInternal(View.java:13715)
android.view.ViewGroup.invalidateChild(ViewGroup.java:5207)
android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1163)
android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)
最后来到了 ViewRootImpl 的 checkThread() 方法:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
ViewRootImpl 是 View 布局树的根对象,它是顶层视图,所以只要它存在并执行到 checkThread() 这个方法时,它都会判断当前线程是否可以更新 UI,不可以就会直接抛出 CalledFromWrongThreadException
错误。
接下来,我们修改代码如下,去除 onCreate() 处的代码,同时在 onResume() 中新增代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
ThreadPool.runOnNonUIThread{
mBinding.vTvRandom.text = "在非UI线程修改UI"
}
}
同学们会报错吗?
这时候路人乙看到代码就这么点,结合我们之前的分析,心里想不能再给甲同学抢了风头,直接原地起跳:简单!这必报错,在非 UI 线程更新 UI,这必报错!不报错我吃。。
这是路人丙及时摁住了乙的没让他说下去,丙说道:根据当前线程池的繁忙程度而定,如果线程池比较忙,子线程需要等待一定时间再执行时就会报错,否则不会报错。
谁对谁错,我们运行一下不就知道了:
可以看到,按钮文字确实从 “随便一点文字” 改成了 “在非UI线程修改UI”,程序也没有报错。
给子线程的运行加上一定的延时,模拟线程池拥堵的情况:
override fun onResume() {
super.onResume()
ThreadPool.runOnNonUIThread({
mBinding.vTvRandom.text = "在非UI线程修改UI"
}, 500)
}
再运行:
程序报错了,事实证明,丙说的没错,那为什么是这样呢?
路人丙淡定地说道:ViewRootImpl 对象的创建时机在于 onResume() 方法之后,在执行 Activity 的 onResume() 方法时,ViewRootImpl 对象还没有被创建,所以不会走到 ViewRootImpl.checkThread() 的地方,自然也就不会报错了。
丙说完,路人甲、乙投来了崇拜的眼神。
是的,在 Activity 的启动过程中,由 ActivityThread 负责 Activity 的创建,Activity 与 Window 的相互绑定,以及各项生命周期方法的调用,比如 onCreate(),onStart(),onResume() 等方法。
在 ActivityThread 中有一个方法:
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// 1. 执行 Activity.onResume()
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 2. 将 DecorView 添加到 window 中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...
}
在这个方法里,Activity 的 onResume() 会被首先调用,然后执行 wm.addView(decor,l)
,将 DecorVeiw 与 WindowManger 绑定,同时创建 ViewRootImpl 对象,执行 decorView 的测量,绘制,布局过程。
// 1. 在初始化 Activity 的 Window 时【Activity.attach()方法】,给 WindowManager 赋值
// 赋值对象是 WindowManagerImpl
// 下面方法在,Window 类
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
// 赋值
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
// 2. 所以 wm.addView(decor, l) 执行的 WindowManagerImpl.addView()
// 下面方法在,WindowManagerImpl 类
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// 3. mGlobal 是个 WindowManagerGlobal 对象
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
// 初始化 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
try {
// 调用 ViewRootImpl.setView() 方法,并把 DecorView 传入
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
...
}
}
}
// 4. 在 ViewRootImpl.setView() 方法中,会执行 ViewRootImpl.requestLayout() 方法
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 从这里开始 DecorView 的测量、绘制、布局过程
scheduleTraversals();
}
}
也就是说 Actvitiy 的生命周期执行到 onResume() 时,ViewRootImpl 对象是还没有创建的。
这也是为什么路人丙说的对的原因。
override fun onResume() {
super.onResume()
// 正常运行
ThreadPool.runOnNonUIThread{
mBinding.vTvRandom.text = "在非UI线程修改UI"
}
// 报错,500ms 后,ViewRootImpl 已经创建完成
ThreadPool.runOnNonUIThread({
mBinding.vTvRandom.text = "在非UI线程修改UI"
}, 500)
}
所以总结一下,在 ViewRootImpl 创建前,未在 UI 线程 更新 UI 也不会报错。那么在 ViewRootImpl 创建后,不在 UI 线程更新 UI,一定会报错吗?
回到之前提到的一个问题,Only the original thread that created a view hierarchy can touch its views.
里的 original thread 是什么意思?原始线程指的是我们的 UI 主线程吗?
这时候路人乙心里又想:刚才这么丢人,不能再那么单纯了,我觉得是不是!如果是 UI 线程,为什么不直接写成 Only the UI thread…。
于是他又站了出来,大声说道:“不是指 UI 线程!如果是值 UI 线程,为什么不直接写成 Only the UI thread… !? 肯定不是,这是的话,我把电脑屏幕吃了!”
这下没人按住他了,路人甲、丙心中也没有明确的答案。
那么到底 original thread 是不是 ui thread 呢?我们看一个例子。
修改原来的代码如下:
override fun onResume() {
super.onResume()
mBinding.vTvRandom.text = "点击进行网络请求"
mBinding.vTvRandom.setOnClickListener {
ThreadPool.runOnNonUIThread({
Looper.prepare()
val dialog = CommonProgressDialog2.show(
this, "正在加载",
null, true, null
)
Looper.loop()
}, 200)
}
}
将原来的按钮名字改成:“点击进行网络请求”,延迟 200 ms 后,在子线程中让界面中间弹出一个加载窗口。
大家觉得正在加载的窗口能够正常弹出吗?
同学甲觉得,show 一个弹窗,也算 ui 操作吧?在子线程中更新 UI,应该会报错。
运行一下:
不可思议,弹窗在非 UI 线程被 show 了出来。
同学们都很不解,接下来还有更奇怪的:
在 Looper.loop() 上面添加下面的代码
dialog?.getLoadingTextView()?.setOnClickListener {
val loadingMsg = dialog.getLoadingMsg()
dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
}
点击“正在加载”区域,更新加载文字,在原来文字的基础上再加个“正在加载”。
注意,此时还是在子线程中进行的操作噢,大家觉得这样会报错吗?
会,会吧?会吗?会不会??同学们都蒙了。
运行一下:
没想到吧,程序还是照常运行。
莫非当前线程都在 UI 线程?我们在程序中加条日志:
Looper.prepare()
val dialog = CommonProgressDialog2.show(
this, "正在加载",
null, true, null
)
// 日志 1
JLog.d(TAG, "runOnNonUIThread is in main thread = ${ThreadPool.isMainThread()}")
dialog?.getLoadingTextView()?.setOnClickListener {
// 日志 2
JLog.d(TAG, "setOnClickListener is in main thread = ${ThreadPool.isMainThread()}")
val loadingMsg = dialog.getLoadingMsg()
dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
}
Looper.loop()
2022-05-09 14:59:54.872 24223-24360/com.example.essay D/UICrashActivity: runOnNonUIThread is in main thread = false
2022-05-09 14:59:58.402 24223-24360/com.example.essay D/UICrashActivity: setOnClickListener is in main thread = false
当前确实没有在 UI 线程。
丙同学陷入了沉思:dialog 里面也有 window,但 dialog 打开显示这么久了,ViewRootImpl 对象不可能还没被创建吧。。
接下来更诡异的,我们修改代码,让更新文字的操作,强制运行在 UI 线程,看下会发生什么?
dialog?.getLoadingTextView()?.setOnClickListener {
// 转至 UI 线程
ThreadPool.runUITask {
JLog.d(TAG, "setOnClickListener is in main thread = ${ThreadPool.isMainThread()}")
val loadingMsg = dialog.getLoadingMsg()
dialog.getLoadingTextView().text = "$loadingMsg, 正在加载"
}
}
报错了。。啊这,看看 log。
2022-05-09 15:06:06.953 24748-24810/com.example.essay D/UICrashActivity: runOnNonUIThread is in main thread = false
2022-05-09 15:06:09.107 24748-24748/com.example.essay D/UICrashActivity: setOnClickListener is in main thread = true
2022-05-09 15:06:09.109 24748-24748/com.example.essay E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.essay, PID: 24748
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7056)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1128)
at android.view.View.requestLayout(View.java:19846)
at android.view.View.requestLayout(View.java:19846)
at android.view.View.requestLayout(View.java:19846)
at android.view.View.requestLayout(View.java:19846)
at android.view.View.requestLayout(View.java:19846)
at android.widget.TextView.checkForRelayout(TextView.java:7375)
at android.widget.TextView.setText(TextView.java:4487)
at android.widget.TextView.setText(TextView.java:4344)
at android.widget.TextView.setText(TextView.java:4319)
at com.jamgu.home.uicrash.UICrashActivity$onResume$1$1$1$1.run(UICrashActivity.kt:60)
at android.os.Handler.handleCallback(Handler.java:754)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:163)
at android.app.ActivityThread.main(ActivityThread.java:6401)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:791)
可见,强制在 UI 线程中修改 UI 也会报错。
似乎,更新 UI 不一定需要在 UI 线程运行。
回过头来,Only the original thread that created a view hierarchy can touch its views.
这里的 original 原始线程,真不是指的 UI 线程。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
ViewRootImpl 里的 mThread 是什么时候被赋值的?我们看下
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
....
mThread = Thread.currentThread();
...
}
mThread 是在 ViewRootImpl 对象被初始化时创建的,所以 checkThread() 中 mThread != Thread.currentThread()
的意思应该是:如果当前的线程与 ViewRootImpl 对象被创建时的线程是同个线程时,checkThread() 通过。
这下明白为什么之前没报错,强制执行在 UI 线程中反而报错了!切至 UI 线程后,当前线程就与 dialog 被创建时的线程不一致了!所以报错了!
原来 UI 更新不一定要在 UI 线程!
日常开发中我们经常通过 runOnUIThread() 方法,将页面更新操作切换至 UI 线程进行也是因为,这些页面本来就是在 UI 线程中被创建的,我们需要将 UI 更新操作切换至对应的线程。
OHHHHH,同学们听完大受震撼,一个长期以来的误区被理清了!这堂课学到非常多,同学们向老师说着一个又一个 “可以,牛逼” ,以此来表达心中的喜悦。
极其帅气的老师心满意足的离开,同学们,下课!
小知识:”可以,牛逼“,以句子简短精炼的优点深受直男喜爱,乃直男表达认可、赞美时经常使用的口头禅,如果你也渐渐开始可以牛逼了,说明你离直男 ❌ 也不远了。
这篇文章到此结束啦,文章读起来应该挺轻松的,希望对你有所帮助~
文章参考
ViewRootImpl源码
View相关问题解惑(ViewRootImpl,PhoneWindow创建时机,View.post为何可以获取View宽高)
Android UI 线程更新UI也会崩溃???
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
- 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!