起因
学过Android开发都听过不能在子线程更新UI,会检查线程从而抛出异常。
但是经过进一步的学习后发现,怎么在onCreate中开启一个线程设置TextView的值,怎么没有报错?甚至不是在onCreate,可能在onResume也一样,这是为什么?
首先要清楚的一点是,在设置TextView 或者addView这些操作,都是会引起requsetLayout(),导致重新布局的,但是翻遍了view的requestLayout()并没有找到所谓的checkThread()方法。但我们发现在view中有这个调用方法。
它去调用了parent的requestLayout(),哦?那是viewGroup中的requestLayout方法有什么特异之处?结果却发现,viewGroup甚至都没有重写requestLayout。
但回想到在正常的view(非dialog,悬浮窗)的顶层viewGroup好像都是DecorView哎,是不是这个有什么不同,看过之后,发现好像就是普通的viewGroup啊。再好好想想,view的绘制流程是不是都是层层往上传递,最后由viewRootImpl处理的?那这个decorView是不是也被windowManger add在ViewRootImpl里面,好像有点道理。
赶紧看看ViewRootImpl的requestLayout,果然在这里发现了checkThread方法,
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
好吧以上都是戏精,在子线程中更新UI后,报错的日志就已经有提示了
2022-07-03 23:08:15.710 25421-25511/com.hua.testcontenprovider E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.hua.testcontenprovider, PID: 25421
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
//从这看出,方法异常是由viewRootImpl的checkThread抛出
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9873)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1933)
at android.view.View.requestLayout(View.java:26151)
at android.view.View.requestLayout(View.java:26151)
at android.view.View.requestLayout(View.java:26151)
at android.view.View.requestLayout(View.java:26151)
at android.view.View.requestLayout(View.java:26151)
at android.view.View.requestLayout(View.java:26151)
at android.view.ViewGroup.addView(ViewGroup.java:5091)
at android.view.ViewGroup.addView(ViewGroup.java:5030)
at android.view.ViewGroup.addView(ViewGroup.java:5002)
at com.hua.testcontenprovider.NewActivity$onResume$1.invoke(NewActivity.kt:99)
at com.hua.testcontenprovider.NewActivity$onResume$1.invoke(NewActivity.kt:97)
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
探索
看了以上的一些提示,恍然大悟了一声,原来是你小子(viewRootImpl)搞得鬼。但疑问再次提出,那为什么onCreate、onResume开启即时线程(开启就运行,无睡眠阻塞的线程)却没问题,这又是咋回事,大家都是setText,凭什么他们搞特殊?别急,我来运行一些代码
override fun onResume() {
super.onResume()
Log.d("TAG", "绘制前DecorView的parent: ${window.decorView.parent}")
linearLayout.postDelayed(
{
Log.d("TAG", "绘制后DecorView的parent: ${window.decorView.parent}")
},
5000L
)
}
有人看完,会说你这是什么意思?难不成在之前viewRootImpl不存在,所以没有检查检查线程?先别下结论,我们看看运行结果。
这结果看下来,好像就是那个意思哈,那这是为什么呢?
那我们得先知道,Activity的onResume的运行时机,这就得去看看ActivityThread了。
代码太多,捡重点看,首先在方法中调用了performResumeActivity()这个方法,这个方法会调用
activity.performResume方法,然后一步一步的回调到我们的Activity的onResume方法。
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
......
// TODO Push resumeArgs into the activity for consideration
// skip below steps for double-resume and r.mFinish = true case.
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
....
final Activity a = r.activity;
......
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
.......
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
.......
}
......
}
看完performResumeActivity()方法调用,接着往下看,代码中获取到activity的window实例,在看过activity的源码中,都知道在activity.attach中new了一个PhoneWindow,而也正是在onCreate回调中,setContentView在window中实例化了decorView,也就是代码运行到此时,phoneWindow、decorView都不为空。
接着判断activity当前window是否添加,activity的mWindowAdded默认值正是false,哦?正所谓拜登大声喊:大的要来了。
我们就看看这个wm.addView中做了什么,以下是截取了部分代码。
WindowManagerGlobal.java
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
原来如此,此时viewRootImpl才初始化,并将其添加到了window管理的所有viewRootImpls中,最后设置了setView,并在该方法中调用了requestLayout(),此时才开始了doTraversal、performTraversal、performMeasure、performLayout、performDraw这些熟悉的view绘制流程。
结论
因为在activity的onResume在执行后,才去实例化ViewRootImpl。以上也就解释了为什么,明明大家都说onResume已经是和用户交互了,为什么还能够在子线程更新UI,并不推荐,只是根据这个来看一些原理,就比如在onResume中获取不到view的宽高,但handler.post延迟了5秒就能获取到了。