Android 窗口机制 SDK31源码分析 总目录
- 初识
- DecorView与SubDecor的创建加载
- Window与Window Manager的创建加载
- ViewRootImpl的创建以及视图真正加载
- ViewRootImpl的事件分发
- 一定要在主线程才可以更新UI吗?为什么?
- Activity的Token和Dialog的关系
- Toast机制 - 封装可以在任何线程调用的toast
- 总结
通过前几章节的介绍,大家应该大致都了解Android整体的窗口机制了吧。
那么问一个问题,UI一定要在主线程才可以更新吗?回答这个问题之前,先看一个简单的例子
示例演示
代码如下所示:代码很简单,在onCreate
中启动子线程(子线程的名字被设置为Myself Thread
)更新TextView
的text
属性,在onResume
函数中将当前的text
值打印在控制台。
lateinit var binding: ActivityThreadTestBinding
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityThreadTestBinding.inflate(layoutInflater)
setContentView(binding.root)
//子线程更新text
Thread {
binding.tv1.text = "The name of the current thread is ${Thread.currentThread().name}."
}.apply {
name = "Myself Thread"
}.start()
}
override fun onResume() {
super.onResume()
//打印出当前控件所设置的text
binding.tv1.text.toString().toLogI()
}
大家可以猜测一下,上述代码可以正常运行吗❓
答案是:可以正常运行,并且控制台打印出来了 The name of the current thread is Myself Thread.
啊😮?这是为什么呢?子线程竟然更新UI竟然没有抛异常而且成功了🤦♀️?不着急,我们慢慢分析。
为什么子线程可以更新UI呢?下面我们分析一下。
我们知道视图更新会调用View的requestLayout
或者invalidate
方法,而这两个方法均会调用ViewRootImpl的requestLayout
方法,进而调用到ViewRootImpl的scheduleTraversals
,好的我们回顾一下ViewRootImpl的requestLayout
方法。
ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//判断当前是否是主线程即mThread,如果不是,则抛异常。就是在子线程更新UI没使用handler的话就会抛出的异常
checkThread();
//设置mLayoutRequested为true。
mLayoutRequested = true;
//进而测量、布局、绘制
scheduleTraversals();
}
}
//判断当前线程如果不是主线程的话,则抛出异常
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
看得到,在ViewRootImpl的checkThread
的方法里面,会进行判断,如果不是主线程的话,就抛出异常(就是我们常见的不能在子线程更新UI的异常)。
那么聪明的同学们可能就要发问了,那为什么上面的代码没有抛异常还运行成功了?
我们再回忆一下前面的章节内容,ViewRootImpl是在什么时候被实例化的呢?
流程大致如下:ActivityThread.handleResumeActivity -> Activity.onResume -> WindowManagerGlobal.addView -> new ViewRootImpl -> ViewRootImpl.setView -> View.assignParent(this)
就是在系统准备显示界面的时候,直观一点的话,就是在Activity的onResume
方法被调用之后,会新创建一个ViewRootImpl
,之后会调用其setView
方法,里面调用到DecorView的assignParent
方法,将其分配给View的mParent
变量。
所以综上所述:在onCreate
方法里面调用设置UI的时候,并没有进行实际的绘制流程,因为ViewRootImpl还没有被设置,那么猜测我们设置的值,应该是被View存在内存了,等到进行真正执行绘制流程的时候,才被渲染出来。
下面进行上述猜测验证,简单分析一下TextView.setText
的流程。
TextView
private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
...
//将value设置到内存
setTextInternal(text)
...
//具体绘制调用
if (mLayout != null) {
checkForRelayout();
}
...
}
private void setTextInternal(@Nullable CharSequence text) {
//内存中储存text值
mText = text;
mSpannable = (text instanceof Spannable) ? (Spannable) text : null;
mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
}
private void checkForRelayout() {
...
//则机会调用view的这两个方法
requestLayout();
invalidate();
}
//View的requestLayout
public void requestLayout() {
...
//mParent指的是ViewRootImpl,如果是在onCreate的时候异步线程更新UI的话,此时mParent为null,所以不会具体的进行绘制,所以就不会抛异常
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
...
}
显而易见,在onCreate
中更新UI时,只是把值保存到了内存中,当视图真正渲染时,才进行正常的绘制流程。
比如如果我们把异步线程setText
的操作,放到一个按钮里面,通过点击实现,那么一定会抛出异常(因为此时界面已经显示出来了)。如下所示:
binding.bt1.setOnClickListener {
Thread {
binding.tv1.text = "The name of the current thread is ${Thread.currentThread().name}."
}.apply {
name = "Myself Thread2"
}.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:9328)
...
at com.pumpkin.automatic_execution.View.ThreadTestActivity.onCreate$lambda-2$lambda-0(ThreadTestActivity.kt:24)
看看抛出的异常堆栈信息了吗?在ViewRootImpl的checkThread
方法。
好了 , 以上的分析就到这里。
那么ViewRootImpl判断的线程,能不能自己设置呢?Dialog就可以设置,想知道怎么操作的小伙伴可以等下一章节噢🙆♀️
创作不易,如有帮助一键三连咯🙆♀️。欢迎技术探讨噢!