This article has authorized the public number “Hongyang” original release.

Hello, I’m Hongyang.

Last weekend was a two-day break, so I decided to change everyone’s perception.

In normal Android development, if a novice comes across an error like this:

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

You, an old bird, with a smile on your lips:

“This is an error caused by not performing UI operations on the UI thread, you can do a UI thread handler post.”

But…

I’m going to say today, is it true that only the UI thread can update the UI?

As an old bird, you must have thought:

TextView#setText = active #onCreate; TextView#setText = active #onCreate;

I read this article years ago, ViewRootImpl hasn’t been created yet.

Look at you so strong, I can’t write this article…

But I’m a man of disobedience. Okay, let me ask you another question:

Does the UI thread update the UI without the above error?

All right, here we go.

The following is the story of a young man writing demand.

Note that the code in this article is written from the perspective of a graduate, so do not refer to it in order to elicit questions and principles, and if you are trying to reproduce the relevant code, it is important to look at every character, even the attributes in the XML.

Little brother needs

The requirements are very simple

  1. Click a button;
  2. The Server sends a problem, and the client Dialog displays it.
  3. Interactively answer questions at Dialog;

Isn’t that a very simple answer.

I wrote a wave of code:

package com.example.testviewrootimpl;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private Button mBtnQuestion;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBtnQuestion = findViewById(R.id.btn_question);

        mBtnQuestion.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { requestAQuestion(); }}); } private voidrequestAQuestion() {
        new Thread(){
            @Override
            public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // Mock server request, return problem String title ="Is Hongyang handsome?";
                showQuestionInDialog(title);
            }
        }.start();
    }

    private void showQuestionInDialog(String title) {
        
    }
}

Copy the code

Simple, click the button, start a new thread to simulate the network request, get the result, and present the problem to the Dialog.

Let’s start writing the code for Dialog:

public class QuestionDialog extends Dialog {

    private TextView mTvTitle;
    private Button mBtnYes;
    private Button mBtnNo;

    public QuestionDialog(@NonNull Context context) {
        super(context);

        setContentView(R.layout.dialog_question); mTvTitle = findViewById(R.id.tv_title); mBtnYes = findViewById(R.id.btn_yes); mBtnNo = findViewById(R.id.btn_no); } public void show(String title) { mTvTitle.setText(title); show(); }}Copy the code

It’s simple. Just a title, two buttons.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24dp"
        android:textStyle="bold"
        tools:text="The ugly one of Hongyang? The ugly one? The ugly one? The ugly one?" />

    <Button
        android:id="@+id/btn_yes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_title"
        android:layout_marginTop="10dp"
        android:text="Yes"></Button>

    <Button
        android:id="@+id/btn_no"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/btn_yes"
        android:layout_alignParentRight="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@id/btn_yes"
        android:text="Not"></Button>

</RelativeLayout>
Copy the code

Then we’ll show it on showQuestionInDialog.

private void showQuestionInDialog(String title) {
    QuestionDialog questionDialog = new QuestionDialog(this);
    questionDialog.show(title);
}
Copy the code

And guess what…

Collapsed…

First crash

Fresh graduate small qi ushered in the breakdown in the first job…

Let’s stop.

The above code is very simple, so I want to ask you why the crash? With all your years of experience.

Guess:

new Thread(){

	puublic void run(){
		show("...");
	}

}

public void show(String title) {
    mTvTitle.setText(title);
    show();
}
Copy the code

TextView#setText = TextView#setText = TextView#setText = TextView#setText

That makes sense. I mean, it’s not like one person would guess that.

Let’s look at the real reason for the error:

Process: com.example.testviewrootimpl, PID: 10544
java.lang.RuntimeException: CanT create handler inside thread thread [thread-2,5,main] that has not called looper.prepare () at android.os.Handler.
      
       (Handler.java:207) at android.os.Handler.
       
        (Handler.java:119) at android.app.Dialog.
        
         (Dialog.java:133) at android.app.Dialog.
         
          (Dialog.java:162) at com.example.testviewrootimpl.QuestionDialog.
          
           (QuestionDialog.java:17) at com.example.testviewrootimpl.MainActivity.showQuestionInDialog(MainActivity.java:46) at com.example.testviewrootimpl.MainActivity.access$100(MainActivity.java:10) at com.example.testviewrootimpl.MainActivity$2.run(MainActivity.java:40)
          Copy the code

Can’t create handler inside thread thread [thread-2,5,main] that has not called looper.prepare ()

I guess it’s wrong, but it’s still a bit familiar. You’ve seen this error before when a child thread plays toast.

As an old bird, you must not be playing the Dialog on the UI thread when you encounter this problem, but the new guy is different.

Blind cats meet dead mice

Dude, just throw the error message into Google, no, Baidu:

Click on the first CSDN blog:

Then quickly add the following to the show Dialog method:

private void showQuestionInDialog(String title) { Looper.prepare(); QuestionDialog = new QuestionDialog(this); questionDialog.show(title); Looper.loop(); // Add part}Copy the code

The solution is as simple as that, with a smile of satisfaction on his lips.

Run the App again…

Let’s stop here for a second.

With years of experience, I want to ask you again: Will it crash again?

Will you?

Guess:

So this code is just a symptom, it’s still not executing the code on the UI thread, it’s still breaking, but we had TextView#setText in our show

That makes sense.

Here’s how it works:

No crash…

Is there a trace of depression?

It doesn’t matter, as an old bird with years of experience, I can always think of the reasons immediately:

As you all know, during active #onCreate, we opened a thread to Text#setText and it didn’t crash, because ViewRootImpl hadn’t been initialized at the time, so it didn’t crash this time.

The corresponding source explanation is as follows:

# Dialog source
public void show() {// omit a bunch of code mWindowManager.addView(mDecor, l); }Copy the code

The Dialog we create for the first time, the first time we call show, does internally execute mWindowManager.addView, which will execute:

# WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

Copy the code

This mGlobal object is WindowManagerGlobal, so let’s look at its addView method:

# WindowManagerGlobal public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); //do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true); } throw e; }}Copy the code

Sure enough, there’s code for new ViewRootImpl immediately, and you can see that ViewRootImpl is not created, so this is the same case as the Activity.

That makes sense…

Let’s move on.

This year’s young brother will continue to do demand.

A hidden problem

The next demand was strange, when asked “Is Hongyang handsome?” If you click no, then the Dialog does not disappear and add another one at the end of the question? The number, so loop, never close.

That’s not a problem for our boy:

mBtnNo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        String s = mTvTitle.getText().toString();
        mTvTitle.setText(s+"?"); }});Copy the code

Operation effect:

Is perfect.

If I ask, do you think there’s something wrong with this code?

You look up a couple of times, and there’s a chicken problem with these two lines of code, maybe there’s a free pointer?

Of course not.

I’ll change the code a little:

mBtnNo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        String s = mTvTitle.getText().toString();
        mTvTitle.setText(s+"?");


        boolean uiThread = Looper.myLooper() == Looper.getMainLooper();
        Toast.makeText(getContext(),"Ui thread = "+ uiThread , Toast.LENGTH_LONG).show(); }});Copy the code

Every time I click, I pop up a Toast that tells me whether the current thread is a UI thread or not.

Look at the effect:

Do you see the problem?

Surprise yourself?

We keep updating the text of the TextView on the non-UI thread.

At this point, you can’t tell me that ViewRootImpl hasn’t been created yet, can you?

Don’t worry…

It gets even more exciting.

More exciting things

Let me change the code again:

private Handler sUiHandler = new Handler(Looper.getMainLooper());

public QuestionDialog(@NonNull Context context) {
    super(context);

    setContentView(R.layout.dialog_question);


    mBtnNo.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            sUiHandler.post(new Runnable() {
                @Override
                public void run() {
                    String s = mTvTitle.getText().toString();
                    mTvTitle.setText(s+"?"); }}); }}); }Copy the code

I’m going to do a UI thread handler, and I’m going to post a Runnable, and I’m going to make sure that our TextView#setText is going to be executed on the UI thread, so it’s very neat and elegant.

Pause again. In your years of experience, will this collapse?

In my blog format, this must have been a demo crash, otherwise the blog would not continue.

That seems to be the case…

Let’s run the effect:

A few clicks, it didn’t crash…

// Caption: Children, do you have a lot of question marks?

As a veteran with many years of experience, I can always think of the reasons immediately:

Of course UI thread updates don’t crash.

Isn’t it?

Let’s click a few more times:

Collapsed…

But it didn’t crash before uihandler. post was added.

As a result, I had to show the code in case you thought I was playing you…

All right, one more stop.

I’m going to ask you one more question, and this time guess what crash?

Are you begging me to stop messing with you and just reveal it.

com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.testviewrootimpl, PID: 18323
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421) at android.view.View.requestLayout(View.java:24434) at android.view.View.requestLayout(View.java:24434) at android.view.View.requestLayout(View.java:24434) at android.view.View.requestLayout(View.java:24434) at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:380)  at android.view.View.requestLayout(View.java:24434) at android.widget.TextView.checkForRelayout(TextView.java:9667) at android.widget.TextView.setText(TextView.java:6261) at android.widget.TextView.setText(TextView.java:6089) at android.widget.TextView.setText(TextView.java:6041) at com.example.testviewrootimpl.QuestionDialogThe $1The $1.run(QuestionDialog.java:38)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7319)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
Copy the code

The familiar figure returned:

Only the original thread that created a view hierarchy can touch its views.

Copy the code

But!

But!

This time it’s thrown from the UI thread.

In response to my first interrogation of the soul:

Does the UI thread update the UI without the above error?

Is not in a muddled and stimulating feeling unable to extricate themselves…

There’s something even more exciting… Well, that’s the end of the story, and we’ll do more exciting things next time.

Don’t be afraid. It’s not over. I have to tell you why.

Little do reveal

In fact, the root of all this lies in our long-term wrong concept.

It’s the UI thread that updates the UI, which is not true. Why is that?

Only the original thread that created a view hierarchy can touch its views.

Copy the code

This exception is thrown in ViewRootImpl, right? Let’s look at this code again:

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

Just a few lines of code, actually.

Let’s take a closer look. His misinformation is not:

Only the UI Thread … Instead, Only the original thread.

If you really want to force Only the Ui Thread, the if statement above should read:

if(UI Thread== Thread.currentThread()){}

Copy the code

Not mThread.

So much for the root cause.

I will take you to look at the source code parsing:

What is this mThread?

Is a member variable of ViewRootImpl, we should focus on when it was assigned:


public ViewRootImpl(Context context, Display display) {
    mContext = context;
    
    mThread = Thread.currentThread();

}
Copy the code

When ViewRootImpl is constructed, the value assigned is the current Thread object.

In other words, the thread in which your ViewRootImpl is created, your subsequent UI updates need to be executed in that thread, regardless of whether it is a UI thread or not.

Corresponding to the above example, we also have a section of pasted source code in the middle.

Just explains:

Dialog’s ViewRootImpl is created when the show() method is executed, and our Dialog’s show is placed in the child thread, so subsequent updates to the View are performed in the child thread.

So that explains why we just switched to the UI thread to do TextView#setText and why it crashed.

Here’s a question to think about. Notice that when we did the demo above, switching to the UI thread and doing setText didn’t crash immediately, it took several times to crash. Why? You want to.

You might have another question:

How does ViewRootImpl relate to a View

In fact, we can find the relevant code by looking at the error stack:

com.example.testviewrootimpl E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.testviewrootimpl, PID: 18323
    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8188)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1421)
        at android.view.View.requestLayout(View.java:24434)

Copy the code

The error stack is triggered by view. requestLayout to ViewRootImpl.

Let’s look directly at this method:

public void requestLayout() {
    
    if(mParent ! = null && ! mParent.isLayoutRequested()) { mParent.requestLayout(); }Copy the code

Notice the mParent variable, which is of type ViewParent interface.

A name knows a meaning.

If I were to ask you what the mParent of a View is, you’d say it’s its parent View, which is a ViewGroup.

Yes, that’s right.

public abstract class ViewGroup extends View implements ViewParent{}
Copy the code

ViewGroup does implement the ViewParent interface.

But there is still a question: who is the mParent of the ViewGroup at the very top of an interface?

Right? It can’t be a ViewGroup, it’s never going to end.

So, ViewParent has another implementation class called ViewRootImpl.

There you have it.

According to the ViewParent system, our interface structure looks like this.

Well, I’ll just write some code:

Again, Dialog, when we click No, we print the ViewParent system:

mBtnNo.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        printViewParentHierarchy(mTvTitle, 0); }}); private voidprintViewParentHierarchy(Object view, int level) {
    if (view == null) {
        return;
    }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < level; i++) {
        sb.append("\t");
    }
    sb.append(view.getClass().getSimpleName());
    Log.d("lmj", sb.toString());

    if (view instanceof View) {
        printViewParentHierarchy(((View) view).getParent(), level + 1); }}Copy the code

Very simply, we print mTbTitle, the ViewParent system all the way up.

D/lmj: AppCompatTextView
D/lmj: 	RelativeLayout
D/lmj: 		FrameLayout
D/lmj: 			FrameLayout
D/lmj: 				DecorView
D/lmj: 					ViewRootImpl
Copy the code

See? Who’s at the bottom.

That’s it, that’s it, that’s it, our ViewRootImpl.

So when your TextView triggers a requestLayout, it’s going to go to the requestLayout of ViewRootImpl, and then to its checkThread, and the checkThread is not just comparing the UI thread to the current thread, Instead, mThread is compared to the current thread.

I think I can end with that.

The next article I may write: Google seems to show us, welcome to pay attention to the article, the specific time is not decided, the thinking is not yet.

One more question: We use Dialog as the example in this article. Can you think of any other examples?

This article tests the device: Android 29 emulator.

Also welcome to follow my public account, wechat search “hongyang”, a bye!