转载请注明来源: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@Overridepublic ViewParent invalidateChildInParent(int[] location, Rect dirty) {checkThread();//省略无关代码}@Overridepublic 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打印233binding.demo1.text="NEW"}
}
这其实已经是在子线程中更新UI,为什么不会抛出异常呢?难道是协程检测到是UI操作自动帮我们切换到了主线程?经过笔者上一篇文章对协程的字节码分析,排除了这种可能。
【Kotlin】协程的字节码原理
难道是页面还没有开始绘制,还没有调用ViewRootImpl.checkThread()
代码吗?那让子线程更新UI操作之前先休眠等待一段时间呢?
lifecycleScope.launchWhenResumed {withContext(Dispatchers.IO) {Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233Thread.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.javaprivate void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {//省略无关代码if (mLayout != null) {checkForRelayout();} //省略无关代码 }
checkForRelayout
方法用来判断是调用invalidate
还是requestLayout
来更新UI。
//TextView.javaprivate 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.javaif (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 pathonDescendantInvalidated(child, child);return;}//省略无关代码}
可以发现,当attachInfo
非空并且开启了硬件加速,那么就走onDescendantInvalidated
流程。View的onDescendantInvalidated
方法最终会递归到ViewRootImpl
的onDescendantInvalidated
方法:
//ViewRootImpl.java@Overridepublic 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();}@UnsupportedAppUsagevoid 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