preface

I believe many readers have read similar articles, but I still want to complete the relationship between the comb clear, good details, I hope you can also learn some.

To get to the point, you’ve probably heard the phrase “UI updates are done in the main thread, and child threads will crash when updating the UI.” Over time this has come to feel like a truism, even considered the “official conclusion.”

But if you were asked where and when this was officially said, you’d be a little confused. And even if it’s official, it’s not always true, right? As we all know, Google’s official documentation is always a bit vague and requires a lot of practice to come to a practical conclusion.

Just like the previous Android11 update document, I also read for a long time, through one practice before writing the adaptation guide, and then found one of the more obvious BUG, Google official said such a sentence:

Here are the first behavioral changes to watch for (regardless of your application’s targetSdkVersion): External storage access – An application can no longer access the files of other applications in the external storage space.

In fact, after practice will find that external storage access is still related to targetSdkVersion, see this guide for Android11 adaptation.

A little too much nonsense. Today, let’s see if the “official conclusion” about threads and UI updates is correct.

In case 1, the child thread updates the button text

1) Update Button display text in onCreate method, change Button width to fixed or wrap_content, neither crash.


    <Button
        android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="I am a button"
        />

        / / or

    <Button
        android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="I am a button"
        />        


    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            btn_ui.text="Young people should teach martial virtue."}}Copy the code

2) The onCreate method has updated the button display text and added a delay.

Button width is fixed and does not crash. Button width wrap_content, crash – Only the original thread that created a view hierarchy can touch its views.


    <Button
        android:id="@+id/btn_ui"
        android:layout_width="100dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="I am a button"
        />

        / / or

    <Button
        android:id="@+id/btn_ui"
        android:layout_width="wrap_content"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:text="I am a button"
        />   
        

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            Thread.sleep(3000)
            btn_ui.text="Young people should teach martial virtue."}}Copy the code

Case 1 Analysis

A little confused feeling, not panic, let’s look at the crash information.

The crash occurred when the button width was wrap_content, which is set to the content, and then 3 seconds later when the button text was updated. In contrast, there are two crash impact points to note:

  • The width of wrap_content. If set to a fixed value, it won’t crash, as shown in Case 2, so does it have to do with the logic of layout changes?
  • Delay 3 seconds. Even wrap_content will not crash if it is not delayed, as shown in Case 1, so is it related to the loading schedule of certain classes?

Take these questions to the source code to find the answer. First look at the crash log:


android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)
        at android.view.View.requestLayout(View.java:24884)
Copy the code

ViewRootImpl requestLayout error () {requestLayout ();

    @Override
    public void requestLayout(a) {
        if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}void checkThread(a) {
        if(mThread ! = Thread.currentThread()) {throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views."); }}Copy the code

Before we solve the mystery, let’s take a look at ViewRootImpl.

ViewRootImpl

From the moment an Activity is created to the moment we see the interface, there are actually two steps: loading the layout and drawing.

  • Loading layout

The setContentView(int layoutResID) method creates a new DecorView and loads a different root layout file according to the activity’s theme or Feature. Finally load the layoutResID resource file. To make it easier for you to understand, I drew a picture:

The final step here is to call the inflate() method of the LayoutInflater, which does only one thing: parses the XML file and then generates the View object based on the node. The result is a complete DOM structure that returns the top-level root layout View. (DOM is a document object model with a hierarchy in which all elements except the top-level elements are included in other element nodes, a bit like a family tree structure, typically for HTML code parsing.)

At this point, a DecorView with a complete view structure is created, but it has not yet been drawn or displayed on the phone interface.

  • draw

The drawing process takes place in the handleResumeActivity. Those familiar with the app startup process should know that the handleResumeActivity method is used to trigger the onResume method, and the DecorView is drawn here as well. Here’s another picture:

  • conclusion

From this we can draw some conclusions: 1) setContentView is used to create a new DecorView and load the layout’s resource file. 2) After the onResume method, create a viewrotimpl that acts as the parent of the DecorView to measure, place, and draw the DecorView. 3) PhoneWindow, as the only subclass of Window, stores and manages DecorView variables and belongs to the middle layer of Activity and View interaction.

Analysis of the collapse

All right. Back to the cause of the crash:


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

CurrentThread is not the mThread that created the view hierarchy. CurrentThread is not the mThread that created the view hierarchy.

The mThread was assigned when the ViewRootImpl was created:

public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}
Copy the code

The viewrotimpl instantiation occurs after the onResume and is used to draw the DecorView to the window.

So we know that the real cause of the crash is that the current thread will crash if it is not the thread that created the ViewRootImpl. Only the original thread that created the view can modify the view. I created you, so I can change you.

Then look at the previous case:

  • In the first case, modify Button in onCreate. In this case, we are only modifying the DecorView. We are not creating the ViewRootImpl, so we are not going to crash the checkThread method. The ViewRootImpl is created after onResume.

  • In case 2, after a delay of 3 seconds, the interface is drawn, the ViewRootImpl is created on the main thread, so the mThread is the main thread, and the Button thread is changed to a child thread, so the setText method will trigger the requestLayout method to redraw, resulting in a crash.

However, the Button width set to a fixed value does not crash. Would the checkThread method not be executed? Strange.

Check setText’s source code to see if a new layout is needed — checkForRelayout()


private void checkForRelayout(a) {
        // 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)) {
            

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

               / /...
            }

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

As you can see, if the layout size does not change, we will not execute requestLayout to redraw the layout. Instead, we will call autoSizeText to calculate the text size and invalidate to draw the text itself, so when we set width and height to a fixed value, The setText() method will not execute to requestLayout(), and the checkThread() method will not execute.

reflection

Why do you need a checkThread to check a thread?

  • Check the thread, which is basically checking that the current thread for the UI update is the same thread that created the UI in the first placeThread safetyThe UI controls themselves are not thread-safe, but locking is too heavy and reduces View loading efficiency because it is interactive. So I’m going to form one directly by the thread logicSingle threaded modelTo ensure thread-safe View operations.

In case 2, the child thread and the main thread showToast respectively

1) onCreate: toast on a thread that has not called Looper. Prepare ()


    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            showToast("Young people should teach martial virtue.")}}Copy the code

2) Add looper.prepare () and looper.loop () to onCreate toast. Don’t collapse.

Plus a 3-second delay, no crash.


    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            //Thread.sleep(3000)

            Looper.prepare()
            showToast("Young people should teach martial virtue.")
            Looper.loop()
        }
    }

Copy the code

3) Use the same Toast instance, click the button before the Toast in the child thread disappears, modify the Toast text in the main thread and display it. Only the original thread that created a view hierarchy can touch its views. Main thread update UI also crashes! You read that right!

Re-run, show and disappear in the child thread, click the button, do not crash.

Change the phone — Samsung S8, re-run, click the button without crashing before the Toast in the child thread disappears.

    lateinit var mToast: Toast

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ui)

        thread {
            Looper.prepare()
            mToast=Toast.makeText(this@UIMainActivity."Young people should teach martial virtue.",Toast.LENGTH_LONG)
            mToast.show()
            Looper.loop()
        }

        btn_ui.setOnClickListener {
            mToast.setText("Rattail juice.")
            mToast.show()
        }
    }
Copy the code

Case 2 Analysis

Before we solve the mystery, let’s have a Toast.

Principle of Toast

Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
Copy the code

A simple and commonly used line of code, or through a flowchart to see how it is created and displayed.

Just like the DecorView loading and drawing process, the layout file is loaded and the View is created. Then, using the addView method, create another instance of ViewRootImpl as the parent to measure the layout and draw.

Collapse analysis

1) The first crash — Can’t toast on a thread that has not called looper.prepare () — the thread creating the toast must have a Looper running.

According to the source code, Toast is shown and hidden through the Handler to transmit messages, so there must be a Handler use environment, that is, binding Looper object, and using the loop method to start the loop processing messages.

2) Second crash — Only the original thread that created a view Hierarchy can touch its views.

The crash here is the same as the previous update to the Button, so it is reasonable to suspect that the requestLayout method of ViewRootImpl was called in a different thread.

When the button is clicked, the mtoast.settext () method is called.

TextView setText() is called, and requestLayout is triggered because the width and height of the TextView in Toast are wrAP_content. Finally, the top View, the requestLayout method of ViewRootImpl, is called.

So the crash happened because Toast created a ViewRootImpl instance the first time he showed the child thread, binding the current thread, the child thread, to the mThread variable. The requestLayout method of ViewRootImpl will be called. The current thread (the main thread) is not the same thread that created the ViewRootImpl instance, so it crashes.

3) Why does clicking the button not crash after Toast disappears?

The Toast hide method is called and the mParent of the View is set to NULL. The requestLayout method is not executed when the setText method is called, and the checkThread is not checked. Post the code:

public void handleHide(a) {
    if(mView ! =null) {
        if(mView.getParent() ! =null) {
            mWM.removeViewImmediate(mView);
        }
        mView = null;
    }
}

removeViewImmediate--->removeViewLocked

private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
    
    / /...
    if(view ! =null) {
        view.assignParent(null);
        if(deferred) { mDyingViews.add(view); }}}void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"+ " it already has a parent"); }}Copy the code

4) But but why doesn’t it crash when you change your phone?

I discovered this by accident, on my Samsung S8, it doesn’t crash at runtime, and instead of changing the Toast text on the current page, the interface gives me feedback like creating a new Toast display, with the setText method written in the instant code.

So I guess on some phones, the Toast Settings have been changed so that when you call setText, the Toast display will end immediately and the hide method will be called. Then revise the Toast text and show it, which is the practice of the third point just now.

Of course, this is just my guess, have studied the mobile phone source god can also be added.

conclusion

Any thread can update the UI, and updating the UI can cause a crash.

The key is whether the thread that drew the view to the interface (i.e. the thread that created the top-level ViewRootImpl) and the thread that updated the UI are the same thread. If not, an error will be reported.

reference

www.jianshu.com/p/1cdd5d1b9…

www.cnblogs.com/fangg/p/129…

Bye bye

Have a study partner can pay attention to my public number – code on the block ❤️❤️ daily three questions knowledge points/interview questions, many a little makes a mickle.