Android中为什么子线程不能更新UI?

1,202 阅读5分钟

Android中子线程真的不能更新UI吗,带着这个疑问,我们可以先在Activty中试一下;

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Thread{
            println("---" + Thread.currentThread().name)
            textView.text = "YYYY"
        }.start()
    }

你也可以自己试一下,运行以上代码,是没有任何问题的,textView中的文字发生了变化。

我们给线程设置一个延时500ms,再次运行代码,程序报错了。

Thread{
    Thread.sleep(500)
    println("---" + Thread.currentThread().name)
    textView.text = "YYYY"
}.start()

异常栈信息:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8913)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1557)
        at android.view.View.requestLayout(View.java:24694)
        at android.view.View.requestLayout(View.java:24694)
        at android.view.View.requestLayout(View.java:24694)
        at android.view.View.requestLayout(View.java:24694)
        at android.view.View.requestLayout(View.java:24694)
        at android.view.View.requestLayout(View.java:24694)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3239)
        at android.view.View.requestLayout(View.java:24694)
        at android.widget.TextView.checkForRelayout(TextView.java:9750)
        at android.widget.TextView.setText(TextView.java:6314)
        at android.widget.TextView.setText(TextView.java:6142)
        at android.widget.TextView.setText(TextView.java:6094)
        at com.cjg.threadupdateview.MainActivity$onCreate$1.run(MainActivity.kt:14)
        at java.lang.Thread.run(Thread.java:929)

设置延时就不行,这是什么原因呢?跟踪一下代码

TextView#setText

@android.view.RemotableViewMethod
    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;
        }
    }

继续跟:

 @UnsupportedAppUsage
    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        mTextSetFromXmlOrResourceId = false;
        if (text == null) {
            text = "";
        }
        ...
         if (mLayout != null) {
            checkForRelayout();
        }

        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);
        ...
    }


 */
    @UnsupportedAppUsage
    private void checkForRelayout() {
        // If we have a fixed width, we can just swap in a new text layout
        // if the text height stays the same or if the view height is fixed.

        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            ...
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    autoSizeText();
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

            // We lose: the height has changed and we have a dynamic height.
            // Request a new view layout using our new text layout.
            requestLayout();
            invalidate();
        } else {
            // Dynamic width, so we have no choice but to request a new
            // view layout with a new text layout.
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

可以看到最终都调用到了requestLayout这个方法

 @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            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;
        }
    }

其调用了父类的requestLayout方法。ViewParent,那么这个ViewParent是谁呢,查看xml布局文件,根布局是RelativeLayout,查看其中代码:

RelativeLayout#requestLayout

@Override
    public void requestLayout() {
        super.requestLayout();
        mDirtyHierarchy = true;
    }

View#requestLayout

@Override
    public void requestLayout() {
       ...
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
       ...
    }

不能再继续跟了,最终的ViewParent到底是谁呢?

其实最终是调用到了ViewRootImpl的requestLayout方法

ViewRootImpl#requestLayout

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

我们看到这里做了线程检查的操作。

ViewRootImpl#checkThread.

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

那为什么一开始在onCreate或者onResume方法中开线程刷新View,没有报错呢?我们继续跟踪一下原因。

我们知道Activity中的View可见是在执行到onResume方法之后,我们从ActivityThread中的handleResumeActivity继续跟踪代码。

 @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        // 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
        //1.这里会最终调用onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        ...
         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) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //2.此处真正将view添加到窗口中
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }
        
    }

我们先看1处,稍后再分析2处,1处调用了performResumeActivity方法:

@VisibleForTesting
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
        String reason) {
    ...
    if (finalStateRequest) {
        r.hideForNow = false;
        r.activity.mStartedActivity = false;
    }
    try {
        r.activity.onStateNotSaved();
        r.activity.mFragments.noteStateNotSaved();
        checkAndBlockForNetworkAccess();
        if (r.pendingIntents != null) {
            deliverNewIntents(r, r.pendingIntents);
            r.pendingIntents = null;
        }
        if (r.pendingResults != null) {
            deliverResults(r, r.pendingResults, reason);
            r.pendingResults = null;
        }
        //调用performResume方法
        r.activity.performResume(r.startsNotResumed, reason);

        r.state = null;
        r.persistentState = null;
        r.setState(ON_RESUME);
        reportTopResumedActivityChanged(r, r.isTopResumedActivity, "topWhenResuming");
    } catch (Exception e) {
        ...
    }
    return r;
}

调用了Activity的performResume方法。

    final void performResume(boolean followedByPause, String reason) {
        dispatchActivityPreResumed();
        performRestart(true /* start */, reason);
        ...
        onPostResume();
        if (!mCalled) {
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onPostResume()");
        }
        ..
         //此处在Instrumentation中调用了onResume方法 
        mInstrumentation.callActivityOnResume(this);
        ..
        dispatchActivityPostResumed();
    }
    

可以看到在handleResumeActivity方法中先执行了Activity的onResume方法,然后在继续跟踪2处代码,其调用执行了wm.addView(decor,l),wm是WindowManager,其具体实现类是WindowManagerImpl,我们查看其中的方法:

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

调用了WindowManagerGlobal中的addView方法;

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
    ...
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    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);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }

可以看到最终是调用了ViewRootImpl中的setView方法。

结论

由以上代码跟踪中我们可以看到,viewRootImpl对象是在Activity中的onResume方法执行完成之后,View变得可见时才创建的,之前的操作是没有进行线程检查的,所以没有报错。但是ViewRootImpl创建之后,由于进行了checkThread操作,所以就不能在子线程更改UI了。

问题

如果不做这个校验,是不是我也是可以正常在子线程更新UI呢?
按理来说,这样是可以的!但是google为什么要这样去设计呢?

(1)如果在不同的线程去控制用一个控件,由于网络延时或者大量耗时操作,会使UI绘制错乱,出了问题也很难去排查到底是哪个线程更新时出了问题;

(2)主线程负责更新,子线程负责耗时操作,能够大大提高响应效率

(3)UI线程非安全线程,多线程进行并发访问有可能会导致内存溢出,降低硬件使用寿命;且非线程安全不能加Lock线程锁,否则会阻塞其他线程对View的访问,效率就会变得低下!