转载请注明来源:https://blog.csdn.net/devnn/article/details/135638486
前言
众所周知,Android App在子线程中是不允许更新UI的,否则会抛出异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
详细异常信息见下图
View的绘制是在ViewRootImpl
中(关于view的绘制流程不是本文重点):
//ViewRootImpl.java
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
//省略无关代码
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
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.");
}
}
问题
笔者偶然发现在协程中是可以更新UI的,比如在Activity的onCreate有以下一段代码:
lifecycleScope.launchWhenResumed {
withContext(Dispatchers.IO) {
Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233
binding.demo1.text="NEW"
}
}
这其实已经是在子线程中更新UI,为什么不会抛出异常呢?难道是协程检测到是UI操作自动帮我们切换到了主线程?经过笔者上一篇文章对协程的字节码分析,排除了这种可能。
【Kotlin】协程的字节码原理
难道是页面还没有开始绘制,还没有调用ViewRootImpl.checkThread()
代码吗?那让子线程更新UI操作之前先休眠等待一段时间呢?
lifecycleScope.launchWhenResumed {
withContext(Dispatchers.IO) {
Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233
Thread.sleep(10000)
binding.demo1.text="NEW"
}
}
经验证也是没有抛出异常。这就有点匪夷所思了!
另外,改用直接使用Thread创建子线程也是同样不会抛异常:
Thread {
Thread.sleep(10000)
val button1 = binding.demo1.text="NEW"
}.start()
这也验证了跟协程是没有关系的。
那只有从源码中寻找答案。
setText流程
看看TextView
的setText
方法的源码。
public void setText(CharSequence text)
方法内部会调到以下4个参数的重载方法。
//TextView.java
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//省略无关代码
if (mLayout != null) {
checkForRelayout();
}
//省略无关代码
}
checkForRelayout
方法用来判断是调用invalidate
还是requestLayout
来更新UI。
//TextView.java
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();
}
}
查看checkForRelayout
方法会发现,当TextView的宽高是写死的,或者宽高跟之前没有变化,那么就调invalidate(),否则调用requestLayout。
笔者经过断点验证,发现在协程中调用的setText方法内部走到以下if语句内部然后return了,这说明宽高没有变化,调用了invalidate()方法来更新UI。
//TextView.java
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
invalildate方法会调用到parent的invalidateChild方法:
//ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
//省略无关代码
}
可以发现,当attachInfo
非空并且开启了硬件加速,那么就走onDescendantInvalidated
流程。View的onDescendantInvalidated
方法最终会递归到ViewRootImpl
的onDescendantInvalidated
方法:
//ViewRootImpl.java
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread();
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
@UnsupportedAppUsage
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
ViewRootImpl的onDescendantInvalidated
方法直接调用了invalidate并没有调用checkThread
方法。
硬件加速默认是开启了,可以使用view的isHardwareAccelerated
方法判断是否开启:
lifecycleScope.launchWhenResumed {
withContext(Dispatchers.IO) {
Thread.sleep(10000)
binding.demo1.text="NEW"
Log.i("MainActivity", "demo1.isHardwareAccelerated:${binding.demo1.isHardwareAccelerated}")
}
}
当给Application配置关闭硬件加速后: android:hardwareAccelerated="false"
以上代码正如所料抛出了异常。
结论
经过以上分析,当使用invalidate
更新UI并且开启了硬件加速,那么是可以在子线程中更新UI的。
还有一种情况就是DecorView
还没有添加到Window中(相当于ViewTree还没有渲染)的情况下,在子线程中也是可以更新UI的,但是更新不会立即生效,因为这个时候ViewRootImpl
还没有创建,比如在onCreate中开启子线程立即更新UI。
转载请注明来源:https://blog.csdn.net/devnn/article/details/135638486