Many beginners will have experienced an error when updating the UI in a child thread of an Activity. Many people know about the mistake, but don’t know what caused it. Today we are going to analyze what causes it. I’m going to use textView updating UI as an example today. Let’s look at the code

<?xml version="1.0" encoding="utf-8"? >
<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"
    tools:context="com.generalwei.mytest.ThreadActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="hello"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:id="@+id/btn_change"
        android:text="Change the UI"/>

</RelativeLayout>Copy the code
public class ThreadActivity extends AppCompatActivity {

    private android.widget.TextView tv;
    private android.widget.Button btnchange;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        this.btnchange = (Button) findViewById(R.id.btn_change);
        this.tv = (TextView) findViewById(R.id.tv);
        btnchange.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run(a) {
                        tv.setText(new Date().getTime()+""); } }).start(); }}); }}Copy the code

After running the app, click the bTNchange button and find that the app crashes. Looking at the log, I found this error:

Take a look at the source code for the tv.settext () method.

public final void setText(CharSequence text) {
        setText(text, mBufferType);
}

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

The checkForRelayout() method is used to check the checkForRelayout() method.

 private voidcheckForRelayout() { ... invalidate(); . }Copy the code

Looking at the invalidate() method, we see a comment like this:

  /** * Invalidate the whole view. If the view is visible, * {@link #onDraw(android.graphics.Canvas)} will be called at some point in * the future. * 

* This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. */

public void invalidate() { invalidate(true); }Copy the code

This method will refresh the view if it is visible. But it has to happen on the UI thread. When we look over here, we see that we have the right track. So let’s see why you have to update the UI on the UI thread.

  void invalidate(boolean invalidateCache) {
        invalidateInternal(0.0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }


void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            ...     
            final ViewParent p = mParent;
            if(p ! =null&& ai ! =null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage); }... }Copy the code

ViewParent is an interface and ViewRootImpl is its implementation class, so let’s go ahead and look at the code as follows:

@Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); . }Copy the code

You’ll find a way to check threads by looking at the checkThread() method.

 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

When we look at the code, we find that there is an mThread Thread, is it a UI Thread? We look at the source of the Thread.

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

The mThread is assigned when the ViewRootImpl is created. The thread must be the UI thread. So an exception is thrown when the current thread is not a UI thread.

Why is it that you can update the UI in the non-UI thread before onResume and sometimes you can update the UI in the non-UI thread, and then we found out that you can update the UI in the non-UI thread before onResume, but you can’t update the UI after onResume. The ActivityThread class has a handleResumeActivity method that calls the Activity’s onResume method back and forth.

 final void handleResumeActivity(IBinder token,boolean clearHide, boolean   isForward, boolean reallyResume, int seq, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if(! checkAndUpdateLifecycleSeq(seq, r,"resumeActivity")) {
        return;
    }

    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

    // TODO Push resumeArgs into the activity for consideration
    r = performResumeActivity(token, clearHide, reason);
    if(r ! =null) {
            if (r.window == null && !a.mFinished && willBeVisible) {
                ...
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if(impl ! =null) { impl.notifyChildRebuilt(); }}if(a.mVisibleFromClient && ! a.mWindowAdded) { a.mWindowAdded =true;
                    wm.addView(decor, l);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if(! willBeVisible) {if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true; }...// Tell the activity manager we have resumed.
           if (reallyResume) {
                try {
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) {
                    throwex.rethrowFromSystemServer(); }}}else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throwex.rethrowFromSystemServer(); }}}Copy the code

We can see such a method performResumeActivity(), its source code is as follows:

public final ActivityClientRecord performResumeActivity(IBinder token,boolean clearHide, Stringreason) { ... if (r ! =null&&! r.activity.mFinished) { ... r.activity.performResume(); . }}Copy the code

You can see that it calls activity.performResume(), so go ahead and look at the source code below:

final voidperformResume() { performRestart(); . mInstrumentation.callActivityOnResume(this); . onPostResume(); . }Copy the code

The performRestart() method is mainly used to call the onRestart method, but the details will not be analyzed. MInstrumentation. CallActivityOnResume () method is to callback the Activity’s OnResume () method. The onPostResume() method this is used to activate the Window. In the handleResumeActivity() method you can see a WindowManager class that controls the window display and addView that adds views. WindowManagerImpl is a WindowManager implementation class, WindowManagerImpl addView method code is as follows:

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }Copy the code

MGlobal is a WindowManagerGlobal object. Continue to look at the code for mglobal.addView:

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) { mRoots.get(i).loadSystemProperties(); }}}}; SystemProperties.addChangeCallback(mSystemPropertyUpdater); } int index = findViewLocked(view,false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

            root = newViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); }... }Copy the code

You can see that the view wrootimPL initialization takes place here, which is why the UI can be updated before onResume. Why do they do that? Because all UI controls are non-thread-safe, updating the UI in a non-UI thread can cause UI clutter. So normally we update the UI in the Handler. If there is any improper writing, please kindly advise.