现象
时常听到传闻"子线程不能更新UI"
1. 运行中常规更新
比如我们声明一个Button
,点击事件中尝试子线程更新UI
fun updateUI() {
Thread {
textView?.text = "Update..."
}.start()
}
结果,果然报错了android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9462)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1776)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3605)
at android.view.View.requestLayout(View.java:25760)
at android.widget.TextView.checkForRelayout(TextView.java:9734)
at android.widget.TextView.setText(TextView.java:6322)
at android.widget.TextView.setText(TextView.java:6150)
at android.widget.TextView.setText(TextView.java:6102)
at com.example.demoapplication.MainActivity$Companion.updateUI$lambda-0(MainActivity.kt:51)
at com.example.demoapplication.MainActivity$Companion.$r8$lambda$Yh_cZvNSUZBQlCsg8jtjl4QkxGQ(Unknown Source:0)
at com.example.demoapplication.MainActivity$Companion$$ExternalSyntheticLambda0.run(Unknown Source:0)
at java.lang.Thread.run(Thread.java:923)
2. 启动时
但是如果我们在onCreate()
、onStart()
、onResume()
之中呢?结果是正常运行,没报错
override fun onResume() {
super.onResume()
Thread {
textView?.text = "New Thread Update onResume()"
}.start()
}
3. Thread.sleep
但是假设我们延迟操作Thread.sleep(1000)
,竟然又报错了
override fun onResume() {
super.onResume()
Thread {
Thread.sleep(1000)
textView?.text = "Update..."
}.start()
}
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9462)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1776)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at android.view.View.requestLayout(View.java:25760)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3605)
at android.view.View.requestLayout(View.java:25760)
at android.widget.TextView.checkForRelayout(TextView.java:9734)
at android.widget.TextView.setText(TextView.java:6322)
at android.widget.TextView.setText(TextView.java:6150)
at android.widget.TextView.setText(TextView.java:6102)
at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:54)
4. Toast或Dialog
当你操作的是Toast
或Dialog
时,又有其他的错误
override fun onResume() {
super.onResume()
Thread {
// 1
textView?.text = "Update..."
// 2
val dialog = Dialog(this)
// 3
Toast.makeText(this, "Hi, Toast.", Toast.LENGTH_LONG).show()
}.start()
}
java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-4,5,main] that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:227)
at android.os.Handler.<init>(Handler.java:129)
at android.app.Dialog.<init>(Dialog.java:133)
at android.app.Dialog.<init>(Dialog.java:162)
at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:50)
at com.example.demoapplication.MainActivity.$r8$lambda$Fpwj7mya3SwKMUmQd_HphQeDxxw(Unknown Source:0)
at com.example.demoapplication.MainActivity$$ExternalSyntheticLambda0.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:923)
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()
at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:157)
at android.widget.Toast.getLooper(Toast.java:179)
at android.widget.Toast.<init>(Toast.java:164)
at android.widget.Toast.makeText(Toast.java:492)
at android.widget.Toast.makeText(Toast.java:480)
at com.example.demoapplication.MainActivity.onResume$lambda-2(MainActivity.kt:52)
原因
先看一下是在哪抛出异常的
// android.view.ViewRootImpl
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
原因出自mThread
,再来看它的赋值时机在哪
// android.view.ViewRootImpl
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
...
mThread = Thread.currentThread();
...
}
那就是说ViewRootImpl
初始化的时候呗,问题变成ViewRootImpl
啥时候初始化了
// android.app.ActivityThread
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
if (r.window == null && !a.mFinished && willBeVisible) {
...
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
...
}
}
// android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
}
ViewRootImpl
对象的创建时机在于onResume()
方法之后 , 更加详情启动流程以后单独分析吧
对于Dialog
和Toast
,构造的时候的Handler
都是默认当前线程的Looper
,Activity
的声明周期是有AMS
调用而Dialog
是应用程序自己调用的。ViewRootImpl
的初始化在Activity
会在onResume()
方法之后,而是Dialog
被调用show
方法时触发的。
如果当前线程的Looper
没有prepare
那么必定会抛异常,如果仅仅执行了prepare
那么崩溃不会产生了,但是依旧不展示。因为整个Looper
还没有开始,里面的Message
都未进行处理。最后我们将代码中注释的 Looper.prepare()
; 和Looper.loop()
; 打开就可以正常在异步线程进行Toast
和Dialog
的展现。
TODO
参考
Android在子线程中操作UI:弹出Toast、改变TextView内容
同学们上课,今天我们学习:UI 操作一定要在 UI 线程吗?