这次彻底搞明白子线程到底能不能更新 UI

4,080 阅读4分钟

本文将从根源上分析,Only the original thread that created a view hierarchy can touch its views 产生的条件以及原因。

有了结论后,会继续分析:

  1. Activity#onCreate()中使用子线程更新 TextView的内容,应用会崩么?
  2. ViewRootImpl初始化完成之后,能在子线程更新 TextView的内容么?
  3. 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.");
      }
  }

mThreadcheckThread()调用者所在线程B不一致就会抛出异常。

那其中的 mThread就是主线程么?通过对其执行 「alt + F7」:

发现mThreadViewRootImp 的构造方法中完成初始化,所以 mThread就是ViewRootImp(...)的调用者所在线程。这时 继续alt + F7」会发现无迹可寻。

可能你对 ViewRootImp 较为陌生,有时间我会从 Activity启动流程出发,讲讲ViewRootImp

先告诉大家较为上层的结论:

  1. AMS会首先将Activity的生命周期事件发送到 ApplicationThread,ApplicationThread接收到事件后会将事件发送至主线程的Handler,即mH,然后执行ActivityThread#handleResumeActivity完成ViewRootImp的初始化, 所以ViewRootImp(...)是在主线程中执行的。
  2. 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,之所以会在文中分析各种奇奇怪怪的用例,只是为了让大家以后如果遇到文章开头那样的异常后,分析的更加从容。