dialog 中的线程

403 阅读2分钟

前言

android 中使用 dialog 时一般情况下涉及到三个方法:new,show 与 dismiss。三个方法都会影响到对线程的使用有影响。

构造函数

在构造一个 dialog 对象时会初始化一个 Handler 对象。因此,dialog 在哪个线程初始化该 handler 就是哪个线程的对象。如果相关的线程没有 Looper::prepare 就会报错。

private final Handler mHandler = new Handler();

如下代码就会报错

// Can't create handler inside thread Thread[xxx] that has not called Looper.prepare()
thread {
    mDialog = Dialog(this)
}

show

show 方法内部会调用 WindowManager::addView(),所以最终会初始化 ViewRootImpl 对象,初始化时也会初始化一些变量。所以如果 show 在子线程中调用,那么 ViewRootImpl 中记录的也是子线程

// 初始化一个 Handler,所以如果 ViewRootImpl 是在子线程初始化的,Handler 就会是子线程的
final ViewRootHandler mHandler = new ViewRootHandler();

// 记录当前线程,也就是 show 方法所在的线程
mThread = Thread.currentThread();

dismiss

dismiss 会进行线程切换

dismiss 源码中会进行线程切换,它保证 dismiss 与构造函数执行在同一个线程,而不是与 show 执行在同一个线程,而且 dismiss 最终可以执行到 ViewRootImpl::checkThread 方法,该方法中又会进行线程判断,它要求当前线程必须和 ViewRootImpl 创建线程是同一个线程

public void dismiss() {
    // mHandler 在是 new dialog 实例的时候创建的
    // 所以此处会将线程切换至 dialog 创建时的线程
    if (Looper.myLooper() == mHandler.getLooper()) {
        dismissDialog();
    } else {
        mHandler.post(mDismissAction);
    }
}

dismiss 最终肯定会执行到 ViewRootImpl 中,将 View 从 window 中移除。在操作之前 ViewRootImpl 会调用 ViewRootImpl::checkThread 判断线程

// ViewRootImpl 中

void checkThread() {
    // mThread 是在 ViewRootImpl 中被赋值,也就是说,它记录的是 show 所在的线程
    
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException("Only the original thread that crea...");
    }
}

总结

下面的总结只适合 android.app 包下的 Dialog,不适用于 androidx,后者会有一些主线程逻辑判断。

  1. dialog 的创建与 show 必须处于同一线程,dismiss 会自动切换至该线程

  2. 整个代码中并没有对主线程的判断,因此 dialog 的 show 可以执行在任何线程中,只要这个线程调用可以使用 Handler 即可

  3. android 允许在子线程中操作 UI,只要保证操作线程也 ViewRootImpl 创建线程是同一个即可。如下代码是可以执行的

子线程操作 UI 示例.png