Android UI Not Crashing When Modifying View off UI Thread

Dung Ta :

Scenario:

I ran into a strange issue while testing out threads in my fragment.

I have a fragment written in Kotlin with the following snippet in 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())
    }
}

is MainThread() is a function that checks if the current thread is the main thread like so:

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

I am seeing my TextView get updated after 2 seconds with the text "Something something : false"

Seeing false tells me that this thread is currently not the UI/Main thread.

I thought this was strange so I created the same fragment but written in Java instead with the following snippet from 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...");
        }
    });
}

The app crashes with the following exception as expected:

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)

I did some research but I couldn't really find something that explains this. Also, please assume that my views are all inflated correctly.

Question:

Why does my app not crash when I modify my TextView in the runnable that's running off my UI thread in the Fragment written in Kotlin?

If there's something in some documentation somewhere that explains this, can someone please refer me to this?

I am not actually trying to modify my UI off the UI thread, I am just curious why this is happening.

Please let me know if you guys need any more information. Thanks a lot!

Update: As per what @Hong Duan mentioned, requestLayout() was not getting called. This has nothing to do with Kotlin/Java but with the TextView itself.

I goofed and didn't realize that the TextView in my Kotlin fragment has a layout_width of "match_parent." Whereas the TextView in my Java fragment has a layout_width of "wrap_content."

TLDR: User error + requestLayout(), where thread checking doesn't always occur.

Hong Duan :

The CalledFromWrongThreadException only throws when necessary, but not always. In your cases, it throws when the ViewRootImpl.checkThread() is called during ViewRootImpl.requestLayout(), here is the code from 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.");
    }
}

And for TextView, it's not always necessary to relayout when we update it's text, we can see the logic in the source code:

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

As you can see, in some cases, the requestLayout() is not called, so the main thread check is not introduced.

So I think the key point is not about Kotlin or Java, it's about the TextViews' layout params which determined whether requestLayout() is called or not.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=69123&siteId=1