Android界面在非UI线程修改View时不会崩溃

6

情境:

在测试片段线程时,遇到了一个奇怪的问题。

我用Kotlin编写了一个片段,在onResume()中包含以下代码块:

override fun onResume() {
    super.onResume()

    val handlerThread = HandlerThread("Stuff")
    handlerThread.start()
    val handler = Handler(handlerThread.looper)
    handler.post {
        Thread.sleep(2000)
        tv_name.setText("Something something : " + isMainThread())
    }
}

isMainThread()是一个函数,用于检查当前线程是否为主线程,如下所示:

private fun isMainThread(): Boolean = Looper.myLooper() == Looper.getMainLooper()

我发现我的TextView在2秒钟后更新,显示的文本是“某些东西:false”。

看到false告诉我这个线程当前不是UI/Main线程。

我觉得这很奇怪,所以我创建了同样的片段,但是用Java编写,使用以下代码从onResume()中提取:

@Override
public void onResume() {
    super.onResume();

    HandlerThread handlerThread = new HandlerThread("stuff");
    handlerThread.start();
    new Handler(handlerThread.getLooper()).post(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            textView.setText("Something something...");
        }
    });
}

应用程序按预期崩溃,并显示以下异常:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7313)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1161)

我做了一些调查,但我并没有找到能够解释这个问题的东西。同时,请假设我的观点都正确地膨胀了。
问题:
在Kotlin编写的Fragment中的UI线程之外运行的runnable中修改TextView时,为什么我的应用程序不会崩溃?
如果有某个文档中解释了这个问题,请引用一下吗?
我实际上并不想在UI线程之外修改我的UI,我只是好奇这是为什么。
如果您需要更多信息,请告诉我。谢谢!
更新: 根据@Hong Duan提到的,requestLayout()没有被调用。这与Kotlin / Java无关,而是与TextView本身有关。
我犯了一个错误,没有意识到我Kotlin片段中的TextView具有“match_parent”的layout_width。而Java片段中的TextView具有“wrap_content”布局宽度。
简而言之,这是用户错误 + requestLayout(),其中线程检查并不总是发生。

1
添加包含“tv_name”和“textView”的布局的XML。 - Hong Duan
2个回答

4

CalledFromWrongThreadException 只有在必要时才抛出,而不总是抛出。在您的情况下,它会在调用 ViewRootImpl.requestLayout() 期间调用 ViewRootImpl.checkThread() 时抛出异常,以下是来自 ViewRootImpl.java 的代码:

@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.");
    }
}

对于TextView,当我们更新它的文本时,并不总是需要重新布局,可以在源代码中看到这个逻辑:

/**
 * Check whether entirely new text requires a new view layout
 * or merely a new text layout.
 */
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; // return with out relayout
            }

            // 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; // return with out relayout
            }
        }

        // 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();
    }
}

如您所见,在某些情况下,不会调用requestLayout(),因此未引入主线程检查。
因此,我认为关键点不是Kotlin或Java,而是TextView的布局参数,这将决定是否调用requestLayout()

没错,那很有道理,明天让我再仔细检查一下我的布局。我现在无法访问代码。 - D Ta

0

很可能,在Kotlin的情况下,setText()存在一些开销,以确保它在UI线程中运行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接