Why can’t I update the UI on a child thread

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

“android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. If you update the UI on a child thread, you get an error, so why can’t you update the UI on a child thread? Is it really possible to update the UI on a child thread?

Check the requestLayout and checkThread of the ViewRootImpl.

public void requestLayout(a) {
    if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code

It executes the checkThread method and throws an exception

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

Many people would think that this method checks the main thread ActivityThread, but if you look at the source code, it checks whether the mThread is the current thread, so you should look at the assignment of the mThread

mThread = Thread.currentThread();
Copy the code

MThread is the thread on which the constructor is called, so whether the method is called by the main thread depends on how the ViewRootImpl is created. If the method is called from an ActivityThread, it is the main thread.

Requestlayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) {requestLayout (checkThread) { RequestLayout of ViewRootImpl is executed.

View::requestLayout

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

View requestLayout () {parent requestLayout () {parent requestLayout () {ViewRootImpl (); And how is that done?

To answer this question, you need to understand the structure of your Activity.

The structure of the Activity page

When developing an Activity, the first step is to pass in the onCreate setContentView to the resource file.

Activity::setContentView Note that this is not analyzing AppCompatActivity

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
Copy the code

The Activity actually calls the setContentView of getWindow(). The Activity first holds a Window, which is an abstract class that has a unique implementation called PhoneWindow, You can think of each Activity as having a PhoneWindow first. There is also a DecorActionBar, which is a ViewStub that sets whether the page contains an ActionBar. ViewStub performs better than setting Invisible.

PhoneWindow::setContentView

public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
    mLayoutInflater.inflate(layoutResID, mContentParent);
Copy the code

Determine if there is contentParent, if not, go to installDecor.

private void installDecor(a) {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if(! mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures ! =0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); }}else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
Copy the code

Here the end generateLayout (mDecor) to the contentParent, new a DecorView generateDecor method. The layoutInflater.inflate method is then called in the PhoneWindow setContentView method to inflate the XML resource file.

To summarize, there is a PhoneWindow built into the Activity. The outermost layer of the PhoneWindow is a DecorView with an ActionBar that is a ViewStub. If the parent of a DecorView is the ViewRootImpl, it depends on how the ViewRootImpl was created.

The ViewRootImpl creation process

ActivityThrad calls performResumeActivity in the handleResumeActivity, and then executes WindowManagerImpl addView, Windows ManagerGlobal’s addView method new view file. The performResumeActivity method calls the activity’s performResume internally, and then executes the Instrumentation callActivityOnResume, This method calls the Activity’s onResume method. One conclusion to draw from this is that the activity has not yet created the page when it executes onResume. handleResumeActivity:

r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); . wm.addView(decor, l);Copy the code

WMGlobal executes the view otimpl setView method after addView

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {
    root.setView(view, wparams, panelParentView, userId);
Copy the code

The ViewRootImpl setView method has a key method, RequestLayout, which is also seen in the initial exception, and then checkThread, initialization process thread is of course consistent, This also answers the question of whether mThread is the main thread, in this case the call stack is clearly the main thread. One of the key methods in requestLayout is scheduleTraversals

void scheduleTraversals(a) {
    if(! mTraversalScheduled) { mTraversalScheduled =true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); }}Copy the code

You can see that an asynchronous task is performed to execute mTraversalRunnable which is also called doTraversal

void doTraversal(a) {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false; }}}Copy the code

Formtraversals is the core method of view, measuring, plotting, and layout. So if I go back to setView, I have another line down here

view.assignParent(this);
Copy the code

Passing the view to the view, which is the argument to the setView, which is the argument to WMGlobal’s addView, which is called by ActivityThread, passes the DecorView

r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); . wm.addView(decor, l);Copy the code

At this point, the ViewRootImpl becomes the parent of the DecorView, so the call stack reporting the error is clear.

Page rendering

Back to performTraversals

booleanlayoutRequested = mLayoutRequested && (! mStopped || mReportNextDraw);if (layoutRequested) {
...
// Ask host how big it wants to be
windowSizeMayChange |= measureHierarchy(host, lp, res,
   desiredWindowWidth, desiredWindowHeight);
Copy the code

It contains a variable mLayoutRequested, which is set to true every time layoutRequest is called and the first setView is invoked. If it is true, getRootMeasureSpec is invoked on measureHierarchy

MeasureSpec is set according to rootDimension. Once the width and height are set, performMeasure is called in the DecorView measure method, where the first measure of the control tree is made.

And then a second measurement, same principle as WRapcontent, because you can’t determine the size with one measurement

Back to the performTraversals method, performLayout is called

        final booleandidLayout = layoutRequested && (! mStopped || mReportNextDraw);boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
Copy the code

The layout method of the DecorView is called inside

        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
Copy the code

Then use the dispatchOnGlobalLayout of the ViewTreeObserver to return the gauge size information

Here mLayoutRequested is set to false, and if this method is reinvoked because of an exception it does not remeasure directly on the draw panel

Back to PerformTraversals

        booleancancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || ! isViewVisible;if(! cancelDraw) {if(mPendingTransitions ! =null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
Copy the code

When the page is not invisible, performDraw is called to draw

In performDraw, the Viewrotimpl draw method is called, and the hardware acceleration is set for ThreadedRender’s draw and the software is set for DecorView’s Draw

How do I update the UI in child threads

Now that we know why we can’t update the UI on the child thread, what do we do if we have to update the UI on the child thread?

requestLayout

Go back to the requestLayout of the View

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

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

PFLAG_FORCE_LAYOUT is a flag that is checked in requestLayout and cleared when the layout is complete. If a requestLayout method is used in the main thread, this flag is set to true. The parent requestLayout is called only when the view flag is false. Requestlayout will not be called to the decorView and will not execute the checkThread.

Handwritten addView

The checkThread method checks the addView thread, which is the main thread when the Decorview is initialized, but you can call the WindowManager to write the addView. So you need to start a looper on the child thread yourself. This will allow child threads to update the UI.

SurfaceView

After all, UI asyncrony and latency can cause a lot of display and interaction problems, and if the above two are risky evils, Android SurfaceView is not. There is a holder in the SurfaceView, which can obtain the Canvas object and draw the UI by itself in the sub-thread. Because of this, SurfaceView has high performance and is commonly used in games, audio and video scenes.