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

UI线程更新UI就不会出现上面的错误了吗?

好了,开讲。

下面是一个应届小哥小奇写需求的故事。

注意本文代码为应届小哥角度所写,为了引出问题及原理,不要随意参考,另外如果尝试复现相关代码,务必看好每一个字符,甚至xml里面的属性都很关键。

小哥的需求

需求很简单,就是

  1. 点击一个按钮;
  2. Server会下发一个问题,客户端Dialog展示;
  3. 在Dialog交互回答问题;

是不是很简答。

小哥怒写一波代码:

package com.example.testviewrootimpl;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

private Button mBtnQuestion;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mBtnQuestion = findViewById(R.id.btn_question);

mBtnQuestion.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestAQuestion();
}
});
}

private void requestAQuestion() {
new Thread(){
@Override
public 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();
}
}

很简答,就一个标题,两个按钮。

然后我们在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: 10544
java.lang.RuntimeException: Can’t create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
at android.os.Handler.(Handler.java:207)
at android.os.Handler.(Handler.java:119)
at android.app.Dialog.(Dialog.java:133)
at android.app.Dialog.(Dialog.java:162)
at com.example.testviewrootimpl.QuestionDialog.(QuestionDialog.java:17)
at com.example.testviewrootimpl.MainActivity.showQuestionInDialog(MainActivity.java:46)
at com.example.testviewrootimpl.MainActivity.access$100(MainActivity.java:10)
at com.example.testviewrootimpl.MainActivity$2.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

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

这个mGlobal对象是WindowManagerGlobal,我们看它的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 things
try {
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() {
@Override
public void onClick(View view) {

String s = mTvTitle.getText().toString();
mTvTitle.setText(s+“?”);
}
});

运行效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很完美。

如果我问,你觉得这个代码有问题吗?

你往上看了几眼,就这两行代码有个鸡儿问题,可能有空指针?

当然不是。

我稍微修改一下代码:

mBtnNo.setOnClickListener(new View.OnClickListener() {
@Override
public 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() {
@Override
public void onClick(View view) {

sUiHandler.post(new Runnable() {
@Override
public void run() {
String s = mTvTitle.getText().toString();
mTvTitle.setText(s+“?”);
}
});
}
});

}

我搞了个UI线程的handler,然后post一下Runnable,确保我们的TextView#setText在UI线程执行,严谨而又优雅。

再停一下,以各位多年经验,这次会崩溃吗?

按照我写博客的套路,这次肯定是演示崩溃呀,不然博客怎么往下写。

好像是这个道理…

点击了几下,没崩…

// 配图:小朋友,你是不是有很多问号。

作为拥有多年经验的老鸟,总能立马想到解释的理由:

UI线程更新当然不会崩溃呀(言语中有一丝不自信)。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

ndroid)**

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter
    [外链图片转存中…(img-rxWX5Oxw-1712026711366)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值