Android 中 Dialog 的 Context 暗藏玄机

今天在开发中遇到了一个关于 DialogContext 的诡异问题。

原本定义了一个 Dialog 的派生类 BaseDialog ,并调用其重载的构造方法设置了 Theme

然后还定义了一个继承 BaseDialogMyDialog ,由于在一个 private 方法中需要用到 Context 的方法,不得已在前面直接声明了一个 Context 对象。

但是在后面调用时发现,由于本身构造方法就传入了一个 Context 对象,实际上存在两个 Context ,因此后面出现了空指针异常。

为了解决这个问题,于是在 BaseDialog 中声明了一个 protected 类型的 Context ,这样 MyDialog 类就不用再去定义全局变量了。

但 Eclipse 提示可以调用 Dialog 类的 getContext() 方法,颇为惊喜和意外,这样看来貌似不用定义 protected 类型的 Context 了。

但一跑发现虽然解决了 NPE ,但是里面自定义组件的 Theme 居然莫名其妙地变了!百思不得其解,后来想到这个 getContext() 方法,一看 Dialog 类源码才发现果然暗藏玄机。

这是其 getContext() 方法:

public final Context getContext() {
    return mContext;
}

乍一看也没啥问题,这应该就是构造方法传入的 Context 啊,那么继续看构造方法:

public Dialog(Context context) {
    this(context, 0, true);
}

public Dialog(Context context, int theme) {
    this(context, theme, true);
}

问题出来了,我们外部调用的构造方法除了 Context ,最多传个 Theme 进来,没想到内部居然都是调用的这个带三个参数的非 public 构造方法:

Dialog(Context context, int theme, boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
        if (theme == 0) {
            TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, outValue, true);
            theme = outValue.resourceId;
        }
        mContext = new ContextThemeWrapper(context, theme);
    } else {
        mContext = context;
    }
    mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    Window w = PolicyManager.makeNewWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}

而且最外层的 if 判断完全没用,因为前面两个构造方法内部传过来都是 true ! 而这个 Context 直接被赋值为封装了 Theme 的对象,早就不是原来的纯粹的 Context 对象了。

这个是最新的 4.4 源码,于是怀疑是不是新的 SDK 才是这样的。再来看看 2.3 版 Dialog 源码:

public Dialog(Context context) {
    this(context, 0);
}

public Dialog(Context context, int theme) {
    mContext = new ContextThemeWrapper(context, theme == 0 ? com.android.internal.R.style.Theme_Dialog : theme);
    mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    Window w = PolicyManager.makeNewWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mUiThread = Thread.currentThread();
    mListenersHandler = new ListenersHandler(this);
}

尼玛这个更坑爹,都没判断,直接调用了 new ContextThemeWrapper()

看来只能乖乖的定义 protected 类型的 Context ,这个 getContext() 方法真是暗藏玄机:

如果你自定义了 DialogTheme ,而且在初始化内部 View 的时候使用的是 getContext() 得到的 Context 对象,那么这个 View 的主题会受到 DialogTheme 影响。