【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

相关推荐
ChinaDragonDreamer1 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
网络研究院3 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下4 小时前
android navigation 用法详细使用
android
小比卡丘6 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭7 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss8 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.9 小时前
数据库语句优化
android·数据库·adb
GEEKVIP11 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model200513 小时前
android + tflite 分类APP开发-2
android·分类·tflite