写点什么

脑瓜子嗡嗡的。。Android-UI- 线程更新 UI 也会崩溃?

用户头像
Android架构
关注
发布于: 6 小时前

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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
脑瓜子嗡嗡的。。Android-UI-线程更新UI也会崩溃?