Generally speaking, child threads cannot update the UI, and an error will be reported if the UI is updated on the child thread.

However, in some cases, it is not wrong to start the thread directly to update the UI.

For example, in the onCreate method, you just start the child thread to update the UI, so there is no error.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    textView = findViewById(R.id.tv)
    thread {
        textView.text = "Ha ha ha ha."}}Copy the code

If there is a delay in child threads, such as adding a line thread.sleep (2000), an error is reported.

Why is that?

One could say, well, since we slept for 2 seconds, the thread checking mechanism of the UI is already set up, so updates in the child thread will get an error.

When did thread detection for UI updates start

The error location for child thread updates is the checkThread method and requestLayout method in ViewRootImpl.

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

CheckThread is a thread detection method, and the call is in the requestLayout method.

To find out when requestLayout is called, how is ViewRootImpl created?

Because in theonCreateCreate child thread access UI, is not error, this also shows inonCreate,ViewRootImplIt has not been created.

When the ViewRootImpl was created.

PerformResumeActivity is called in the ActivityThread’s handleResumeActivity for the onResume callback.

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
    // code omitted...
    
    // performResumeActivity eventually calls the Activity's onResume method
    // The call chain is as follows: r.activity. PerformResume is called.
    // performResumeActivity -> r.activity.performResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    
    // code omitted...
        
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) {
            // Notice that the activity displays, and the ViewRootImpl will eventually be createdr.activity.makeVisible(); }}}Copy the code

Follow up activity.makevisible ().

void makeVisible(a) {
    if(! mWindowAdded) { ViewManager wm = getWindowManager();// Add a DecorView to WindowManager
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}
Copy the code

WindowManager is an interface whose implementation class is WindowManagerImpl.

// WindowManagerImpl addView method
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    // WindowManagerGlobal addView is finally called
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

/ / WindowManagerGlobal addView
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    // Omit some code
	
    // The ViewRootImpl object declaration
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // Omit some code

        // Create the ViewRootImpl object
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        
        try {
            // Call the ViewRootImpl setView method
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throwe; }}}Copy the code

As you can see, the ViewRootImpl is created by Windows ManagerGlobal’s addView method after the activity’s onResume method is called.

therequestLayoutHow is it called?

In the Windows ManagerGlobal addView method above, after the ViewRootImpl is created, its setView method is called, and requestLayout is called inside the setView method.

This will detect the thread that was called when the UI was updated.

/ / ViewRootImpl setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;

            // omit irrelevant code...
            // Call to requestLayout
            requestLayout();
        
          // omit irrelevant code...}}/ / requestLayout method
@Override
public void requestLayout(a) {
    if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code

In the SheduleTranversals method, the run method of the TraversalRunnable is called, and finally in the performTraversals method, Call performMeasure performLayout performDraw to start the View drawing process.

void scheduleTraversals(a) {
    if(! mTraversalScheduled) { mTraversalScheduled =true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // TraversalRunnable run enables the UI measure, layout, and draw
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       // omit irrelevant code...}}final class TraversalRunnable implements Runnable {
    @Override
    public void run(a) { doTraversal(); }}void doTraversal(a) {
    if (mTraversalScheduled) {
        // Omit some codeperformTraversals(); }}private void performTraversals(a) {
    // Ask host how big it wants to be
    // Omit some code
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}
Copy the code

The child thread updates the UI

Now that you know that the child thread updates the UI in the checkThread method, is there any way to bypass it? Can you do child threads update the UI?

The answer is yes.

I’ll try it out with a simple demo, but let’s see what it looks like.

The code is as follows:

// MainActivity
public class MainActivity extends AppCompatActivity {
    private View containerView;
    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
    private TextView mTv2;
    private TextView mTv1;

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

        containerView = findViewById(R.id.container_layout);
        mTv1 = findViewById(R.id.text);
        mTv2 = findViewById(R.id.text2);

        // Start the thread and GlobalLayoutListener
        Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
    }

    private void initGlobalLayoutListener(a) {
        globalLayoutListener = () -> {
            Log.e("caihua"."onGlobalLayout : " + Thread.currentThread().getName());
            ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
            containerView.setLayoutParams(layoutParams);
        };
        this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
    }


    public void updateUiInMain(View view) {
        mTv1.setText("Main thread updates UI");
    }

    public void updateUiInThread(View view) {
        new Thread(){
            @Override
            public void run(a) {
                SystemClock.sleep(2000);
                mTv2.setText("Child thread updates UI:"+ Thread.currentThread().getName()); } }.start(); }}Copy the code

Principle: Through the ViewTreeObserver. OnGlobalLayoutListener set the global layout to monitor, and then in onGlobalLayout approach, called the view of setLayoutParams method, The setLayoutParams method calls requestLayout internally to bypass thread detection.

Why can we get around that?

becausesetLayoutParamsIn the callrequestLayoutThe method is notViewRootImplrequestLayout.

whileViewrequestLayoutDon’t callcheckThreadMethod to detect threads.

Source code: ↓

/ / the setLayoutParams source code
public void setLayoutParams(ViewGroup.LayoutParams params) {
    if (params == null) {
        throw new NullPointerException("Layout parameters cannot be null");
    }
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) {
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    }
    Call the requestLayout method.
    requestLayout();
}
// View requestLayout method
public void requestLayout(a) {
    if(mMeasureCache ! =null) mMeasureCache.clear();

    if(mAttachInfo ! =null && mAttachInfo.mViewRequestingLayout == null) {
        ViewRootImpl viewRoot = getViewRootImpl();
        if(viewRoot ! =null && viewRoot.isInLayout()) {
            if(! viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

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

The original link: caihuasay.com/posts/threa…