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

今天是回家的日子,希望可以在途中完成这篇随笔~

不知道大家有没有这种感受,每次回家都会不由自主的去总结这一年的得失,计算自己的年龄,焦虑不久的未来,甚至对过往也不会放过。包括学生时代也是这样~好吧,说白了就是矫情吧。

这也或许是一种"仪式",就像春节一样,只不过这只是关乎于一个人~不知道外国人对于"仪式"有怎样的理解,至少对于我们而言,小到自我矫情,家庭聚餐,大到结婚典礼,国家会议,"仪式"相当重要,当然我们也天然继承着这种基因,我特意查阅学习到,这种基因就来自于"华夏"两字-中国有礼仪之大故称夏,有服章之美谓之华。

这篇随笔想要分享一个小的知识点-非UI线程如何更新UI

我相信一定有小伙伴觉得这篇随笔在"胡扯",或者认为即使子线程可以更新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() {
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方法做了什么事情:

 
    
    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()的作用分别是什么以及两个的区别。

(以上,雪人自述)

© 著作权归作者所有
这个作品真棒,我要支持一下!
本专栏将主要分为以下几个分部: 1,深入分析Android系统源码,包括四大组件,动画源码以及时间事件分发机制等...
2条评论

猜一波
1.不能
2.onresume后
3.invalidate 就走draw
requestlayout measure layout draw都走一遍

雪人
#2

#1楼 @Jimmy Chung 哈哈,兄弟你都说了ViewRootImpl创建是在onResume后,所以当然也可以在onResume实现子线程更新UI

top Created with Sketch.