【Android】为什么在子线程中更新UI不会抛出异常

转载请注明来源: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的绘制流程不是本文重点):

java 复制代码
//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有以下一段代码:

java 复制代码
  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操作之前先休眠等待一段时间呢?

java 复制代码
  lifecycleScope.launchWhenResumed {
        withContext(Dispatchers.IO) {
        Log.i("MainActivity", "launchWhenResumed,threadId:${Thread.currentThread().id}")//threadId打印233
        Thread.sleep(10000)
        binding.demo1.text="NEW"
 	}
}

经验证也是没有抛出异常。这就有点匪夷所思了!

另外,改用直接使用Thread创建子线程也是同样不会抛异常:

java 复制代码
 Thread {
     Thread.sleep(10000)
     val button1 = binding.demo1.text="NEW"
 }.start()

这也验证了跟协程是没有关系的。

那只有从源码中寻找答案。

setText流程

看看TextViewsetText方法的源码。
public void setText(CharSequence text)方法内部会调到以下4个参数的重载方法。

java 复制代码
//TextView.java
  private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
       //省略无关代码
      if (mLayout != null) {
            checkForRelayout();
       }    
      //省略无关代码           
   }

checkForRelayout方法用来判断是调用invalidate还是requestLayout来更新UI。

java 复制代码
//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。

java 复制代码
//TextView.java
    if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }

invalildate方法会调用到parent的invalidateChild方法:

java 复制代码
//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方法最终会递归到ViewRootImplonDescendantInvalidated方法:

java 复制代码
//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方法判断是否开启:

kotlin 复制代码
 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

相关推荐
SRC_BLUE_1740 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
内蒙深海大鲨鱼5 小时前
qt之ui开发
数据库·qt·ui
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto