脑瓜子嗡嗡的。。Android-UI- 线程更新 UI 也会崩溃?
private Button mBtnQuestion;
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
mBtnQuestion = findViewById(R.id.btn_question);
mBtnQuestion.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {requestAQuestion();}});}
private void requestAQuestion() {new Thread(){@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 模拟服务器请求,返回问题 String title = "鸿洋帅气吗?";showQuestionInDialog(title);}}.start();}
private void showQuestionInDialog(String title) {
}}
很简单吧,点击按钮,新启动一个线程去模拟网络请求,结果拿到后,把问题展示在 Dialog。
下面开始写 Dialog 的代码:
public class QuestionDialog extends Dialog {
private TextView mTvTitle;private Button mBtnYes;private Button mBtnNo;
public QuestionDialog(@NonNull Context context) {super(context);
setContentView(R.layout.dialog_question);
mTvTitle = findViewById(R.id.tv_title);mBtnYes = findViewById(R.id.btn_yes);mBtnNo = findViewById(R.id.btn_no);
}
public void show(String title) {mTvTitle.setText(title);show();}}
很简答,就一个标题,两个按钮。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent">
<TextViewandroid:id="@+id/tv_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="24dp"android:textStyle="bold"tools:text="鸿洋丑的一匹?鸿洋丑的一匹?鸿洋丑的一匹?鸿洋丑的一匹?" />
<Buttonandroid:id="@+id/btn_yes"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@id/tv_title"android:layout_marginTop="10dp"android:text="是的"></Button>
<Buttonandroid:id="@+id/btn_no"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignTop="@id/btn_yes"android:layout_alignParentRight="true"android:layout_marginLeft="20dp"android:layout_toRightOf="@id/btn_yes"android:text="不是"></Button>
</RelativeLayout>
然后我们在 showQuestionInDialog 让它 show 出来。
private void showQuestionInDialog(String title) {QuestionDialog questionDialog = new QuestionDialog(this);questionDialog.show(title);}
你们猜结果怎么着...
崩溃了...
第一次崩溃
应届生小齐迎来了第一次工作中的崩溃...
我们先停下来。
上面的代码很简单吧,那么我想问各位为什么会崩溃呢?凭各位多年的经验。
猜想:
new Thread(){
puublic void run(){show("...");}
}
public void show(String title) {mTvTitle.setText(title);show();}
上面 new Thread 模拟数据,没有切到 UI 线程就 show Dialog 了,而且执行了 TextView#setText,肯定是在非 UI 线程更新 UI 导致的。
很有道理,绝不是一个人会这么猜测吧。
下面我们看真正报错的原因:
Process: com.example.testviewrootimpl, PID: 10544java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()at android.os.Handler.<init>(Handler.java:207)at android.os.Handler.<init>(Handler.java:119)at android.app.Dialog.<init>(Dialog.java:133)at android.app.Dialog.<init>(Dialog.java:162)at com.example.testviewrootimpl.QuestionDialog.<init>(QuestionDialog.java:17)at com.example.testviewrootimpl.MainActivity.showQuestionInDialog(MainActivity.java:46)at com.example.testviewrootimpl.MainActivity.access2.run(MainActivity.java:40)
Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
虽然猜错了,但是依旧有点熟悉的感觉,以前大家在子线程弹 toast 的时候是不是见过类似的错误。
作为一个老鸟,遇到这个问题,肯定是不在 UI 线程弹 Dialog,但是应届小哥就不同了。
瞎猫遇到死耗子
小哥,直接把报错信息扔进 Google,不,百度:
点开第一篇 CSDN 的博客:
然后迅速举一反三,在刚才 show Dialog 的方法中增加:
private void showQuestionInDialog(String title) {Looper.prepare(); // 增加部分 QuestionDialog questionDialog = new QuestionDialog(this);questionDialog.show(title);Looper.loop(); // 增加部分}
解决问题就是这么简单,嘴角露出一丝对自己满意的笑容。
再次运行 App...
这里大家再停一下。
凭各位多年的经验,我想再问一句,这次还会崩溃吗?
会吗?
猜想:
这代码治标不治本,还是没有在 UI 线程执行相关代码,还是会崩,而却刚才的 show 里面还有 TextView#setText 操作
有点道理。
看一下运行效果:
没有崩溃...
是不是有一丝的郁闷?
没关系,作为拥有多年经验的老鸟,总能立马想到解释的理由:
大家都知道在 Activity#onCreate 的时候,我们开个线程去执行 Text#setText 也不会崩溃,原因是 ViewRootImpl 那时候还没初始化,所以这次没崩溃也是一个原因。
对应源码解释是这样的:
Dialog 源码
public void show() {
// 省略一堆代码 mWindowManager.addView(mDecor, l);}
我们首次创建的 Dialog,第一次调用 show 方法,内部确实会执行 mWindowManager.addView,这个代码会执行到:
WindowManagerImpl
@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);}
这个 mGlobal 对象是 WindowManagerG
lobal,我们看它的 addView 方法:
WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {// 省略了一堆代码 root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);
mViews.add(view);mRoots.add(root);mParams.add(wparams);
// do this last because it fires off messages to start doing thingstry {root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.if (index >= 0) {removeViewLocked(index, true);}throw e;}}
果然立马有 new ViewRootImpl 的代码,你看 ViewRootImpl 没有创建,所以这和 Activity 那个是一个情况。
好像有那么点道理哈...
我们继续往下看。
应届小哥要继续做需求了。
一个隐藏的问题
接下来的需求很奇怪,就是当询问"鸿洋帅气吗?"的时候,如果你点击不是,那么 Dialog 不消失,在问题的末尾再加一个?号,如此循环,永不关闭。
这难不倒我们的小哥:
mBtnNo.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {
String s = mTvTitle.getText().toString();mTvTitle.setText(s+"?");}});
运行效果:
很完美。
如果我问,你觉得这个代码有问题吗?
你往上看了几眼,就这两行代码有个鸡儿问题,可能有空指针?
当然不是。
我稍微修改一下代码:
mBtnNo.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {
String s = mTvTitle.getText().toString();mTvTitle.setText(s+"?");
boolean uiThread = Looper.myLooper() == Looper.getMainLooper();Toast.makeText(getContext(),"Ui thread = " + uiThread , Toast.LENGTH_LONG).show();}});
每次点击的时候,我弹了个 Toast,输出当前线程是不是 UI 线程。
发现问题了吗?
出乎自己的意料吗?
我们在非 UI 线程一直在更新 TextView 的 text。
这个时候,你不能跟我扯什么 ViewRootImpl 还没有创建了吧?
别急...
还有更刺激的。
更刺激的事情
我再改一下代码:
private Handler sUiHandler = new Handler(Looper.getMainLooper());
public QuestionDialog(@NonNull Context context) {super(context);
setContentView(R.layout.dialog_question);
mBtnNo.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {
sUiHandler.post(new Runnable() {@Overridepublic void run() {String s = mTvTitle.getText().toString();mTvTitle.setText(s+"?");}});}});
}
我搞了个 UI 线程的 handler,然后 post 一下 Runnable,确保我们的 TextView#setText 在 UI 线程执行,严谨而又优雅。
再停一下,以各位多年经验,这次会崩溃吗?
按照我写博客的套路,这次肯定是演示崩溃呀,不然博客怎么往下写。
好像是这个道理...
点击了几下,没崩...
// 配图:小朋友,你是不是有很多问号。
作为拥有多年经验的老鸟,总能立马想到解释的理由:
UI 线程更新当然不会崩溃呀(言语中有一丝不自信)。
是吗?
我们多点击几次:
崩溃了...
但是刚才在没有添加 UiHandler.post 之前可没有崩溃哟。
这个结果,我都得把代码露出来了,怕你们说我演你们...
好了,再停一停。
我又要问大家一个问题了,这次你猜是什么崩溃?
是不是求我别搞你们了,直接揭秘吧。
com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: mainProcess: com.example.testviewrootimpl, PID: 18323android.view.ViewRootImpl1MethodAndArgsCaller.run(RuntimeInit.java:492)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
那个熟悉的身影回来了:
Only the original thread that created a view hierarchy can touch its views.
但是!
但是!
这次可是在切换到 UI 线程抛出来的。
对应我开头的灵魂拷问:
UI 线程更新 UI 就不会出现上面的错误了吗?
是不是在一股懵逼又刺激的感觉中无法自拔...
还有更刺激的事情...嗯,篇幅问题,本篇我们就到这了,更刺激的事情我们下次再写。
别怕,没完,我总得告诉你们为什么吧。
小做揭秘
其实这一切的根源都在于我们长久的一个错误的概念。
就是 UI 线程才能更新 UI,这是不对的,为什么这么说呢?
Only the original thread that created a view hierarchy can touch its views.
这个异常是在 ViewRootImpl 里面抛出的对吧,我们再次来审视一下这段代码:
void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}
其实就几行代码。
我们仔细看一下,他这个错误信息并不是:
Only the UI Thread ... 而是 Only the original thread。
评论