Android 中 Dialog 的 Context 暗藏玄机

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

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

然后还定义了一个继承 BaseDialog 的 MyDialog ,由于在一个 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() 方法真是暗藏玄机:

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