Android 子线程更新TextView的text 不抛出异常原因 分析总结

今天有同事问我,在Android中子线程是不是不可以直接操作View组件

我毫不犹豫的回答“是”

接着他让我看一段代码 大致如下

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.setContentView(R.layout.activity_scroll_textview);
    tv = findViewById(R.id.tv);

    new Thread() {
        @Override
        public void run() {
            super.run();
            tv.setText("xsxxxx");
        }
    }.start();

同事说 这段代码跑起来不会崩溃,我好奇的自己写了一段试了一下,果然没崩溃

于是仔细研究了一下子线程更新UI的崩溃原理

在常见情况下,如果我们在子线程中更新UI就会抛出如下异常

android.view.ViewRootImpl$CalledFromWrongThreadException	Only the original thread that created a view hierarchy can touch its views.
ViewRootImpl.java	7979
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7979)
	at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1253)
	at android.view.View.requestLayout(View.java:23305)
	at android.view.View.requestLayout(View.java:23305)
	at android.view.View.requestLayout(View.java:23305)
	at android.view.View.requestLayout(View.java:23305)
	at android.view.View.requestLayout(View.java:23305)
	at android.widget.ScrollView.requestLayout(ScrollView.java:1603)
	at android.view.View.requestLayout(View.java:23305)
	at android.view.View.requestLayout(View.java:23305)
	at android.widget.TextView.checkForRelayout(TextView.java:8957)
	at android.widget.TextView.setText(TextView.java:5754)
	at android.widget.TextView.setText(TextView.java:5588)
	at android.widget.TextView.setText(TextView.java:5545)
	at com.base.collections.extensions.sample.ui.TestChainActivity$1.run(TestChainActivity.java:31)

可以看出这是View 主动抛出了一个自定义异常

我们在源码里面全局查找这段异常

发现 其实是在 ViewRootImpl 类的checkThread()方法中 检测了当前操作方法的Thread是否与 ViewRootImpl创建时所在的线程是同一个线程

ViewRootImpl在这里是由框架创建的,因此这里进行的就是主线程的检测

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

checkThread调用的地方如图

看这个崩溃堆栈 ,可以发现 在setText时,触发了checkForRelayout 方法 

最终调用ViewRootImpl的 requestLayout方法,在requestLayout方法中 使用 checkThread()最终抛出了异常

我们看一下setText源码

源码很长 这里精简了一下

    private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
        if (text == null) {
            text = "";
        }

        mBufferType = type;
        mText = text;

        ...............................


        if (mLayout != null) {
            checkForRelayout();
        }

        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);

        ...............................
        
        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
        }


    }

checkForRelayoutde 判断条件 mLayout 是否为NULL

mLayout 是在 onMeasure 方法中获取的

所以我们得到了第一个可能不崩溃的原因,TextView 还没有被加入到ViewTree中去

再看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) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    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)) {
                    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 不是固定的宽高,那么根据输入的字符大小数量,TextView 需要动态的计算宽高 否则只要直接绘制就可以了

这时候我们得到了第二个可能不崩溃的原因

如果一个TextView 设置的是固定宽高 这个时候在设置Text的时候 就不需要requestLayout,所以就不会触发checkThread()方法

继续往下看

虽然没有调用requestLayout,但是还是调用了 invalidate();方法,在文章的开头,我们观察checkThread()方法时,提到ViewRootImpl调用checkThread的场景方法,其中有一个是 invalidateChildInParent 这样的话按照我的理解,即使没有在requestLayout的时候崩溃,那么总该在invalidate的时候蹦了吧。真的是这样吗?继续来看源码

from View.class   
 public void invalidate() {
        invalidate(true);
    }

    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {

            。。。。。。。。。。。。。
            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);
            }

            。。。。。。。。。。。。。
        }
    }

最终走到了ViewParent的invalidateChild 方法中

from ViewGroup.class   
 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;
        }
        .................................................................
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
                parent = parent.invalidateChildInParent(location, dirty);
                if (view != null) {
                    // Account for transform on current parent
                    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的过程中是分两种情况的,开启硬件加速和不开启硬件加速

先看不开启硬件加速的场景,主要就是走了 parent.invalidateChildInParent ,这是一个do while 循环

    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            .............
            return mParent;
        }
        return null;
    }

invalidateChildInParent 方法会将自己的mParent 返回 并进入下一次循环,这样的最终就会走到ViewRootImpl的invalidateChildInParent ,然后就是checkThread 再然后就是抛出异常

于是我们将demo中的硬件加速关闭,来测试这种场景,果然抛出了异常,证明在非硬件加速情况下这种流程是对的

至于开启了硬件加速之后不抛出异常这种场景,由于对硬件加速原理了解甚微,这里就不多做研究了

总结:

在子线程中给TextView setText 不会抛出异常的两个场景

1:TextView 还没来得及加入到ViewTree中

2:TextView已经被加入了ViewTree,但是被设置了固定宽高,且开启了硬件加速

子线程操作View 确实不一定导致Crash,那是因为刚好满足一定的条件并没有触发checkThread机制,但这并不代表我们在开发过程中可以这么写,谨记

发布了24 篇原创文章 · 获赞 3 · 访问量 6134

猜你喜欢

转载自blog.csdn.net/binghelonglong123/article/details/95333516