onCreate中直接开启子线程操作UI,为什么不会崩溃


今天在写一篇博客时,想贴上一段不能在子线程操作 UI 的异常信息,于是故意写了这么一段代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    textView = (TextView) findViewById(R.id.text_view);
    new Thread(){
        @Override
        public void run() {
            // 子线程中操作 UI
            textView.setText("text_view_thread" + "--" + Thread.currentThread().getName());
        }
    }.start();
}

上面代码在子线程中给 TextView 设置 text,和想象中不同的是,不但没有奔溃,反而设置成功了!!!黑人问号.jpg

明明是在非 UI 线程中操作 UI,为什么不奔溃呢?简直颠覆了我的知识点,直接表现为对基础的深入理解不到位啊。通过 @王帅 的解惑知道了原因。这里感谢 @王帅。看来要多向 dalao 们学习了。

那么原因是什么呢?让我们一步步从源码中寻找答案。

通常我们在子线程中操作 UI,系统都会抛出这样一个运行时异常:

android.view.ViewRootImpl$CalledFromWrongThreadException: 
        Only the original thread that created a view hierarchy can touch its views.

从这段异常信息中可以看出,异常类是 ViewRootImpl$CalledFromWrongThreadException。我们到 ViewRootImpl 中找到该异常类,发现只有在 checkThread() 中使用了该异常类:

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

mThread 的值是当前线程,由于 ViewRootImpl 是在 ActivityThread 中创建的,所以 mThread 代表是主线程。checkThread() 方法是用于检测当前线程是否为 ActivityThread,如果不是则抛出异常,并且 ViewRootImpl 中和 UI 操作相关的方法都调用了 checkThread() 来检测当前线程是主线程。这就是为什么不能在子线程中操作 UI 的原因。

我们再来看看 ViewRootImpl 是什么时候创建的,ViewRootImpl 的创建决定了 checkThread() 的调用时机。

android.view.WindowManagerImpl.java

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

WindowManager 的 addView 方法调用了 WindowManagerGlobal 的 addView 方法。

android.view.WindowManagerGlobal.java

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

WindowManagerGlobal 的 addView 方法代码比较长,我们只关注 ViewRootImpl 的创建,可以知道 ViewRootImpl 其实相当于是在 WindowManager 的 addView 方法中创建的。

继续寻找 addView 的调用时机。

android.app.ActivityThread.java

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String 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 && !a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
            
}

在 ActivityThread 中的 handleResumeActivity 中调用了 addView 方法,也就是说从这里开始创建了上面的 ViewRootImpl 对象。

 public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                ...
                case RESUME_ACTIVITY:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
                    SomeArgs args = (SomeArgs) msg.obj;
                    handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true,
                            args.argi3, "RESUME_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                ...
            }
            Object obj = msg.obj;
            if (obj instanceof SomeArgs) {
                ((SomeArgs) obj).recycle();
            }
            if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
        }

handleMessage 方法是用来处理系统的各种事件,包括 Activity 的生命周期。有很多 case 语句,我们只看 case RESUME_ACTIVITY,在这里处理的是 Activity 的 resume 生命周期。看到这答案已经见分晓了,原因就在于 ViewRootImpl 是在 OnResume 事件发生时才创建的,onCreate 执行时,并不存在 ViewRootImpl 对象,也就不能调用 ViewRootImpl#checkThread 方法来检测当前线程是不是主线程,所以 onCreate 中直接开启子线程操作 UI 是不会崩溃的。

参考文章:

Android子线程真的不能更新UI么

评论