本文将从根源上分析,Only the original thread that created a view hierarchy can touch its views 产生的条件以及原因。
有了结论后,会继续分析:
Activity#onCreate()中使用子线程更新TextView的内容,应用会崩么?ViewRootImpl初始化完成之后,能在子线程更新TextView的内容么?TextView.setText()引起的checkThread()只能通过requestLayout()触发。
checkThread()调用条件
Only the original thread that created a view hierarchy can touch its views通过checkThread()抛出。
通过对 checkThread() 执行 「alt + F7」发现:
其中我们最熟悉的就是 requestLayout() 和 invalidate()
invalidate()的调用链中会走到invalidateChildInParent()。
分析invalidate()时需要特别注意:
即开启硬件加速的情况下,
invalidate()会走特殊流程后直接 return 并不会调用 checkThread()
target API 级别为 14 及更高级别,则硬件加速默认处于启用状态
基于以上分析, 得出结论:
requestLayout() 和 未开启硬件加速的invalidate()会触发checkThread()
其实硬件加速我们基本都不会关闭,只有在自定义view时,当使用了硬件加速不支持的API时才会关掉。
checkThread()什么情况下会抛异常
先看源码:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
当mThread 和 checkThread()调用者所在线程B不一致就会抛出异常。
那其中的 mThread就是主线程么?通过对其执行 「alt + F7」:
发现mThread在 ViewRootImp 的构造方法中完成初始化,所以 mThread就是ViewRootImp(...)的调用者所在线程。这时
继续alt + F7」会发现无迹可寻。
可能你对
ViewRootImp较为陌生,有时间我会从Activity启动流程出发,讲讲ViewRootImp
先告诉大家较为上层的结论:
AMS会首先将Activity的生命周期事件发送到ApplicationThread,ApplicationThread接收到事件后会将事件发送至主线程的Handler,即mH,然后执行ActivityThread#handleResumeActivity完成ViewRootImp的初始化, 所以ViewRootImp(...)是在主线程中执行的。ViewRootImp可触发绘制流程,具体可见ViewRootImp的实例方法。
基于以上分析, 得出结论:
checkThread() 被触发时,创建View的线程和更新View内容所在的线程不一致时,就会抛异常。
分析几个实际问题
Activity#onCreate()中使用子线程更新 TextView 内容崩不崩?
情景一:
不会崩。此时
ViewRootImp 并未完成实例化,更别说调用其实例方法 checkThread()了。
情景二:
崩。此时
ViewRootImp 已经实例化完成,更新内容的过程中如果调用了实例方法 checkThread(),就会崩。
言外之意,就是可能不会调用
checkThread(),详见后文。
ViewRootImpl初始化完成后,能在子线程更新UI么?
能,代码如下:
运行后可以发现非主线程
non-ui-thread 中也能更新UI。
TextView.setText()引起的checkThread()只能通过requestLayout()触发?
TextView.setText()通过checkForRelayout()完成UI更新。
@UnsupportedAppUsage
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
源码表明,当TextView 的宽高不变时,调用了invalidate()而非requestLayout(), 结合本文前部分的结论,此时如果开启了硬件加速,就不会调用checkThrea()。 所以当我们在 Activity#onCreate() 中,在子线程中对宽高一定的TextView执行setText(...)时,应用不会崩溃。
最后
why
分析了那么多,留个大家一个思考,checkThread() 的意义是什么, 如果没有这个方法会怎样呢。
写这篇文章的动机
写这篇文章的初衷就是,郭神最近发了震惊!Android子线程也能修改UI?,其中的内容不够有说服力,并且评论区里也有些同学提出了自己的疑问,所以我在其基础上作了补充,并且还想说的是 ViewRootImp 可以在任何线程创建,并不局限于主线程。
再啰嗦两句
在平时的开发过程中,我们基本都是在主线程中老老实实的更新UI,之所以会在文中分析各种奇奇怪怪的用例,只是为了让大家以后如果遇到文章开头那样的异常后,分析的更加从容。