android:text=“是的”>
然后我们在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线程更新怎么会崩溃呀(言语中有一丝不自信)。
是吗?
我们多点击几次:
崩溃了…
但是刚才在没有添加UiHandler.post之前可没有崩溃哟。
这个结果,我都得把代码露出来了,怕你们说我演你们…
好了,再停一停。
我又要问大家一个问题了,这次你猜是什么崩溃?
是不是求我别搞你们了,直接揭秘吧。
com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.testviewrootimpl, PID: 18323
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)
at android.view.View.requestLayout(View.java:24434)
at android.view.View.requestLayout(View.java:24434)
at android.view.View.requestLayout(View.java:24434)
at android.view.View.requestLayout(View.java:24434)
at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:380)
at android.view.View.requestLayout(View.java:24434)
at android.widget.TextView.checkForRelayout(TextView.java:9667)
at android.widget.TextView.setText(TextView.java:6261)
at android.widget.TextView.setText(TextView.java:6089)
at android.widget.TextView.setText(TextView.java:6041)
at com.example.testviewrootimpl.QuestionDialog$1
1.
r
u
n
(
Q
u
e
s
t
i
o
n
D
i
a
l
o
g
.
j
a
v
a
:
38
)
a
t
a
n
d
r
o
i
d
.
o
s
.
H
a
n
d
l
e
r
.
h
a
n
d
l
e
C
a
l
l
b
a
c
k
(
H
a
n
d
l
e
r
.
j
a
v
a
:
883
)
a
t
a
n
d
r
o
i
d
.
o
s
.
H
a
n
d
l
e
r
.
d
i
s
p
a
t
c
h
M
e
s
s
a
g
e
(
H
a
n
d
l
e
r
.
j
a
v
a
:
100
)
a
t
a
n
d
r
o
i
d
.
o
s
.
L
o
o
p
e
r
.
l
o
o
p
(
L
o
o
p
e
r
.
j
a
v
a
:
214
)
a
t
a
n
d
r
o
i
d
.
a
p
p
.
A
c
t
i
v
i
t
y
T
h
r
e
a
d
.
m
a
i
n
(
A
c
t
i
v
i
t
y
T
h
r
e
a
d
.
j
a
v
a
:
7319
)
a
t
j
a
v
a
.
l
a
n
g
.
r
e
f
l
e
c
t
.
M
e
t
h
o
d
.
i
n
v
o
k
e
(
N
a
t
i
v
e
M
e
t
h
o
d
)
a
t
c
o
m
.
a
n
d
r
o
i
d
.
i
n
t
e
r
n
a
l
.
o
s
.
R
u
n
t
i
m
e
I
n
i
t
1.run(QuestionDialog.java:38) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7319) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit
1.run(QuestionDialog.java:38)at android.os.Handler.handleCallback(Handler.java:883)at android.os.Handler.dispatchMessage(Handler.java:100)at android.os.Looper.loop(Looper.java:214)at android.app.ActivityThread.main(ActivityThread.java:7319)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInitMethodAndArgsCaller.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。
对吧,如果真的想强制为Only the Ui Thread,上面的if语句应该写成:
if(UI Thread != Thread.currentThread()){}
而不是mThread。
根本原因说完了。
我再带大家看下源码解析:
这个mThread是什么?
是ViewRootImpl的成员变量,我们重点应该关注它什么时候赋值的:
public ViewRootImpl(Context context, Display display) {
mContext = context;
mThread = Thread.currentThread();
}
在ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象。
也就是说,你ViewRootImpl在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。
对应到上面的例子,我们中间也有段贴源码的地方。
恰好说明了:
Dialog的ViewRootImpl,其实是在执行show()方法的时候创建的,而我们的Dialog的show放在子线程里面,所以导致后续View更新,执行到ViewRootImpl#checkThread的时候,都在子线程才可以。
这就说明了,为什么我们刚才切到UI线程去执行TextView#setText为啥崩了。
这里有个思考题,注意我们上面演示的时候,切到UI线程执行setText没有立马崩溃,而是执行了好几次之后才崩溃的,为什么呢?自己想。
大家可能还有个一问题:
ViewRootImpl怎么和View关联起来的
其实我们看报错堆栈很好找到相关代码:
com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.testviewrootimpl, PID: 18323
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)
at android.view.View.requestLayout(View.java:24434)
报错的堆栈都是由View.requestLayout触发到ViewRootImpl的。
我们直接看这个方法:
public void requestLayout() {
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
注意里面这个mParent变量,它的类型是ViewParent接口。
见名知意。
我要问你一个View的mParent是什么,你肯定会回答是它的父View,也就是个ViewGroup。
对,没错。
public abstract class ViewGroup
extends View
implements ViewParent{}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/795249f758382a34abf3307c050b6321.jpeg)
最后说一下我的学习路线
其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:
- 架构师筑基必备技能
- Android框架体系架构(高级UI+FrameWork源码)
- 360°Androidapp全方位性能调优
- 设计思想解读开源框架
- NDK模块开发
- 移动架构师专题项目实战环节
- 移动架构师不可不学习微信小程序
- 混合开发的flutter
Android学习的资料
我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。
330页PDF Android学习核心笔记(内含上面8大板块)
Android学习的系统对应视频
总结
我希望通过我自己的学习方法来帮助大家去提升技术:
-
1、多看书、看源码和做项目,平时多种总结
-
2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
-
3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
-
4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
Android学习核心笔记(内含上面8大板块)**
[外链图片转存中…(img-T53zMBFT-1713623438497)]
Android学习的系统对应视频
总结
我希望通过我自己的学习方法来帮助大家去提升技术:
-
1、多看书、看源码和做项目,平时多种总结
-
2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理
-
3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习
-
4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!
希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!