非 UI 线程真的不能更新 UI 吗?

1,339 阅读3分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

首先先看一个例子:

TextView tv;

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main3);

    tv = findViewById(R.id.tv);



    new Thread(new Runnable() {

        @Override

        public void run() {

            tv.setText("非UI线程更新TextView");

        }

    }).start();

}

例子很简单,在就在Activity的onCreate()方法中开启一个线程,在线程中更新TextView的内容。然后运行程序,“出乎意料”的事情发生了,程序正常运行。所以结论是:子线程中可以更新UI。

上面的结论是正确的,不用怀疑,但是答案并不重要。我们在看一个例子:

TextView tv;

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main3);

    tv = findViewById(R.id.tv);



    new Thread(new Runnable() {

        @Override

        public void run() {

            try {

                Thread.sleep(2000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            tv.setText("非UI线程更新TextView");

        }

    }).start();

}

这个例子和第一个例子基本相同,不同的是在run方法中加了Thread.sleep(2000),让线程先睡2秒。程序刚启后并没有报错,2秒后程序崩了,报错的信息很熟悉:

所以子线程中又不能更新UI了。
那么如果换成Thread.sleep(100)呢?换成Thread.sleep(10)呢?

我们都知道上述的报错信息,一定是因为这句话:

    tv.setText("非UI线程更新TextView")

因此需要查看setText()都做了哪些事情。

@android.view.RemotableViewMethod

public final void setText(CharSequence text) {

    setText(text, mBufferType);

}

继续查看setText(text, mBufferType):

public void setText(CharSequence text, BufferType type) {

    setText(text, type, true, 0);



    if (mCharWrapper != null) {

        mCharWrapper.mChars = null;

    }

}

依旧没有有价值的信息,继续查看setText(text, type, true, 0):

 private void setText(CharSequence text, BufferType type,

                         boolean notifyBefore, int oldlen) {

        mTextFromResource = false;

        if (text == null) {

            text = "";

        }

        if (!isSuggestionsEnabled()) {

            text = removeSuggestionSpans(text);

        }



        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);



        if (text instanceof Spanned

                && ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {

            if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {

                setHorizontalFadingEdgeEnabled(true);

                mMarqueeFadeMode = MARQUEE_FADE_NORMAL;

            } 

         ……

         ……

         ……

      if (mLayout != null) {

            checkForRelayout();

        }



        sendOnTextChanged(text, 0, oldlen, textLength);

        onTextChanged(text, 0, oldlen, textLength);

        ……

        ……

        ……

在第23行我们发现了checkForRelayout()这个方法,这是我们所要找的:

   private void checkForRelayout() {



        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT

                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))

                && (mHint == null || mHintLayout != null)

                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {



            int oldht = mLayout.getHeight();

            int want = mLayout.getWidth();

            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,

                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),

                          false);



            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {



                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT

                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {

                    autoSizeText();

                    invalidate();

                    return;

                }

                if (mLayout.getHeight() == oldht

                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {

                    autoSizeText();

                    invalidate();

                    return;

                }

            }

            requestLayout();

            invalidate();

        } else {

            nullLayouts();

            requestLayout();

            invalidate();

        }

    }

checkForRelayout()中代码不算太多,就一并复制到了这里。我们可以看到不管在30行还是34行,都会调用 requestLayout(), invalidate()两个方法,到这里我们应该多去再思考一个问题,毕竟这两个方法对于我们并不陌生,并且还时常一起出现。

requestLayout(), invalidate()的作用分别是什么以及两个的区别

不过对于本文来说,我们需要关注的是 invalidate():

public void invalidate() {

    invalidate(true);

}

继续查看 invalidate(true):

public void invalidate(boolean invalidateCache) {

    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

}

继续查看invalidateInternal():

 void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

            boolean fullInvalidate) {

        if (mGhostView != null) {

            mGhostView.invalidate(true);

            return;

        }



        if (skipInvalidate()) {

            return;

        }



        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)

                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)

                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED

                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {

            if (fullInvalidate) {

                mLastIsOpaque = isOpaque();

                mPrivateFlags &= ~PFLAG_DRAWN;

            }



            mPrivateFlags |= PFLAG_DIRTY;



            if (invalidateCache) {

                mPrivateFlags |= PFLAG_INVALIDATED;

                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            }

            final AttachInfo ai = mAttachInfo;

            final ViewParent p = mParent;

            if (p != null && ai != null && l < r && t < b) {

                final Rect damage = ai.mTmpInvalRect;

                damage.set(l, t, r, b);

                p.invalidateChild(this, damage);

            }





            if (mBackground != null && mBackground.isProjected()) {

                final View receiver = getProjectionReceiver();

                if (receiver != null) {

                    receiver.damageInParent();

                }

            }

        }

    }

这里我们需要关注的是28-33行,重点关注28行的ViewParent p,然后32行调用了p.invalidateChild(this,damage),这里的ViewParent就是TextView的父布局,这里假设其父布局是LinearLayout,然后我们再查看下父布局中invalidateChild方法做了什么事情:

 @Deprecated

    @Override

    public final void invalidateChild(View child, final Rect dirty) {

        final AttachInfo attachInfo = mAttachInfo;

        if (attachInfo != null && attachInfo.mHardwareAccelerated) {



            onDescendantInvalidated(child, child);

            return;

        }



        ViewParent parent = this;

        if (attachInfo != null) {



            final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;

            Matrix childMatrix = child.getMatrix();

            final boolean isOpaque = child.isOpaque() && !drawAnimation &&

                    child.getAnimation() == null && childMatrix.isIdentity();



            int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;



            if (child.mLayerType != LAYER_TYPE_NONE) {

                mPrivateFlags |= PFLAG_INVALIDATED;

                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            }



            final int[] location = attachInfo.mInvalidateChildLocation;

            location[CHILD_LEFT_INDEX] = child.mLeft;

            location[CHILD_TOP_INDEX] = child.mTop;

            if (!childMatrix.isIdentity() ||

                    (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {

                RectF boundingRect = attachInfo.mTmpTransformRect;

                boundingRect.set(dirty);

                Matrix transformMatrix;

                if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {

                    Transformation t = attachInfo.mTmpTransformation;

                    boolean transformed = getChildStaticTransformation(child, t);

                    if (transformed) {

                        transformMatrix = attachInfo.mTmpMatrix;

                        transformMatrix.set(t.getMatrix());

                        if (!childMatrix.isIdentity()) {

                            transformMatrix.preConcat(childMatrix);

                        }

                    } else {

                        transformMatrix = childMatrix;

                    }

                } else {

                    transformMatrix = childMatrix;

                }

                transformMatrix.mapRect(boundingRect);

                dirty.set((int) Math.floor(boundingRect.left),

                        (int) Math.floor(boundingRect.top),

                        (int) Math.ceil(boundingRect.right),

                        (int) Math.ceil(boundingRect.bottom));

            }



            do {

                View view = null;

                if (parent instanceof View) {

                    view = (View) parent;

                }



                if (drawAnimation) {

                    if (view != null) {

                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                    } else if (parent instanceof ViewRootImpl) {

                        ((ViewRootImpl) parent).mIsAnimating = true;

                    }

                }

                if (view != null) {

                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&

                            view.getSolidColor() == 0) {

                        opaqueFlag = PFLAG_DIRTY;

                    }

                    if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {

                        view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;

                    }

                }



                parent = parent.invalidateChildInParent(location, dirty);

                if (view != null) {



                    Matrix m = view.getMatrix();

                    if (!m.isIdentity()) {

                        RectF boundingRect = attachInfo.mTmpTransformRect;

                        boundingRect.set(dirty);

                        m.mapRect(boundingRect);

                        dirty.set((int) Math.floor(boundingRect.left),

                                (int) Math.floor(boundingRect.top),

                                (int) Math.ceil(boundingRect.right),

                                (int) Math.ceil(boundingRect.bottom));

                    }

                }

            } while (parent != null);

        }

    }

其实父布局中invalidateChild()方法定义在ViewGroup中,这里我们重点看56行到93行,也就是do{ } while{ }中,在79行不停地调用parent = parent.invalidateChildInParent(location, dirty),对于invalidateChildInParent方法其实这里不用太关注,只需要知道它不停地返回其父布局就可以了,最终会返回根布局ViewRootImpl,然后调用ViewRootImpl中invalidateChildInParent()方法:


@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

    checkThread();

    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

    if (dirty == null) {

        invalidate();

        return null;

    } else if (dirty.isEmpty() && !mIsAnimating) {

        return null;

    }

    if (mCurScrollY != 0 || mTranslator != null) {

        mTempRect.set(dirty);

        dirty = mTempRect;

        if (mCurScrollY != 0) {

            dirty.offset(0, -mCurScrollY);

        }

        if (mTranslator != null) {

            mTranslator.translateRectInAppWindowToScreen(dirty);

        }

        if (mAttachInfo.mScalingRequired) {

            dirty.inset(-1, -1);

        }

    }

    invalidateRectOnScreen(dirty);

    return null;

}

第三行看到方法 checkThread():

 void checkThread() {

    if (mThread != Thread.currentThread()) {

        throw new CalledFromWrongThreadException(

                "Only the original thread that created a view hierarchy can touch its views.");

    }

}

这个方法很简单,但是也非常重要,它就是在判断当前线程是否是主线程,如果不是抛出异常,也就是开篇第二个例子中抛出的异常信息。

以上:我们得知,当执行TextView.setText()方法时,首先会执行 invalidate()方法,进而会得到ViewRootImpl,然后会执行到checkThread()方法来判断当前线程是否是主线程。

通过以上分析我们可以得出两个结论:

  1. Android系统通过checkThread()方法来阻止开发者在子线程中更新UI
  2. checkThread()方法定义在ViewRootImpl中

到这里我们就可以解释开篇第一个例子中为什么在子线程中也可以更新UI,是因为在Activity的onCreate()方法中ViewRootImpl对象还没有创建,那么也就不可能执行checkThread()方法,因此可是达到在子线程中更新UI的目的。在第二个例子中增加了Thread.sleep(2000),当2秒过去时ViewRootImpl对象已经创建完毕了,因此也就不能在子线程中更新UI了。

最后我再给出第三个例子:

@Override

protected void onResume() {

    super.onResume();

    new Thread(new Runnable() {

        @Override

        public void run() {

            tv.setText("非UI线程更新TextView2");

        }

    }).start();

}

问:

  1. 第三个例子,将onCreate()改为onResume(),能否实现在子线程中更新UI?
  2. ViewRootImpl对象到底在什么时候创建的?
  3. requestLayout(), invalidate()的作用分别是什么以及两个的区别

公众号:程序员喵大人(专注于Android各类学习笔记、面试题以及IT类资讯的分享。)