background

Today, I made a function, which registered a broadcast to monitor wifi connection, displayed a floating Textview after the connection, and set a Button outside, the user can transfer some content to the floating Textview to display after clicking, everything was so smooth, but when it was running, there was an error:

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                      at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
                      at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
                      at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
                      at android.view.View.invalidateInternal(View.java:13656)
                      at android.view.View.invalidate(View.java:13620)
                      at android.view.View.invalidate(View.java:13604)
                      at android.widget.TextView.checkForRelayout(TextView.java:7347)
                      at android.widget.TextView.setText(TextView.java:4480)
                      at android.widget.TextView.setText(TextView.java:4337)
                      at android.widget.TextView.setText(TextView.java:4312)
        at com.renny.demo.view.activity.HomeActivity$onCreate$1.onClick(HomeActivity.kt:57)
Copy the code

HomeActivity is where I send the data.

This error, which I encountered a few times in my early Android days, is now so familiar that it took me 1.25 seconds to figure out what the problem was, and I must have been updating the View in the child thread. Well, I can’t see what the child thread is after looking at the code, but I’m smart enough to throw it to the main thread with handler:

 new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run(a) {
                mTextviw.setText("hello"); }});Copy the code

Unfortunately, the problem remains, it seems that things are not simple.

Analysis of the

When ViewRootImpl is created, the Thread that created it is assigned to the mThread member variable.

 public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();
        mThread = Thread.currentThread();
Copy the code

When we set the text content to the TextView, we check whether mLayout is empty or not, and if not, we call the checkForRelayout() method.

private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
    // ......
    if(mLayout ! = null) { checkForRelayout(); } / /... }Copy the code

The mLayout will be assigned at the first onMeasure. Now the Textview is displayed first, so it is not empty at this time. This is later called in checkForRelayout()

private void checkForRelayout() {
    if(...). { requestLayout(); invalidate(); }else{ nullLayouts(); requestLayout(); invalidate(); }}Copy the code

When a View (or ViewGroup) calls invalidate(), the parent View’s invalidateChild(View child, final Rect dirty) is called. Every cycle will invoke the parent = parent. InvalidateChildInParent (location, dirty), until to the outside View nodes.

Calling the invalidateChildInParent method of ViewRoot when traversing the root node, ViewRoot, is actually calling the invalidateChildInParent method of ViewRoot’s implementation class ViewRootImpl. This mParent is ViewRootImpl when the invalidateChild(View Child, final Rect Dirty) method ina ViewGroup is looped to the outermost layer. The thread is checked when ViewRootImpl’s invalidateChildInParent(int[] location, Rect dirty) method is called, which is checkThread().

CheckThread () checks whether the current thread is the one that created the ViewRootImpl and raises an exception if it is not.

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

So just see if the ViewRootImpl thread is the main thread. Adding a hover window creates a ViewRootImpl:

        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val lp = WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
                gravity = Gravity.LEFT or Gravity.TOP
            }
            textView = TextView(applicationContext)
            windowManager.addView(textView, lp)
Copy the code

Create this in the addView method:

 public void addView(View view, LayoutParams params) {
        //...
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        this.mViews.add(view);
        this.mRoots.add(root);
        this.mParams.add(wparams);
    }
Copy the code

The thread that created the suspension window is not the main thread. Because when you sign up for broadcast:

 mContext.registerReceiver(mReceiver, mFilter, null, mWorkHandler)
Copy the code

So onReceive will be called back in the thread I specified.

conclusion

The view can only be updated in the UI thread, yes, but the UI thread is not necessarily the main thread, it’s the thread that created the ViewRootImpl, or more specifically, the thread that windowManager added the RootView, and when you add a view in the child thread, The child thread is the UI thread that the view and the child view (if any) are in, and we can’t update the view in any other thread, including the main thread. Finally, the requirement for the UI thread is to make sure the loop is enabled:

        thread {
            Looper.prepare()
                   val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val lp = WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
                gravity = Gravity.LEFT or Gravity.TOP
            }
            textView = TextView(applicationContext)
            windowManager.addView(textView, lp)
            Looper.loop()
        }
Copy the code

reference

Only the original thread that created a view hierarchy can touch its views. How did it come about