安卓 AlertDialog 报 android.content.res.Resources$NotFoundException 的坑

最近项目中想简单实现一个两个项目的Dialog,却一直报如题的错误。

起因是这样的:

写了个弹出以文本作为内容的AlertDialog类,想做一个简单弹窗选择。

public class SimpleDialogUtils {

    public static void showSimpleChooseDialog(int itemsId, DialogInterface.OnClickListener listener) {
        AlertDialog dialog = new AlertDialog
                .Builder(Utils.getApp())
                .setItems(itemsId, listener)
                .create();
        dialog.show();
    }
}

注意这里:.Builder(Utils.getApp()) ,Bulider需要传入一个context作为参数。
这里我自作聪明,把全局的Application传进去了。这里就导致了问题。

使用时,如下进行调用:

public class EditProfileActivity extends BaseActivity {
    @Override
    protected int initLayoutRes() {
        return R.layout.activity_edit_my_profile;
    }

    @OnClick(R.id.btn_change_pic)
    void showChoose(){
        SimpleDialogUtils.showSimpleChooseDialog(R.array.pic_choose
                , new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {

            }
        });
    }
}

运行,crash~

看log提示资源未找到,还以为values目录下的arrays.xml中的文本资源有问题,后来改为用写死的String[] 作为参数的形式仍然未果。

这里写图片描述

思考一下,发现log显示的 android.content.res.Resources$NotFoundException: Resource ID #0x0

资源id应该不是文本资源引起的,id为0,可能是父view的资源没有引用到。

查看了Dialog源码,发现创建Dialog的Builder初始化时,会解析主题

public Builder(@NonNull Context context) {
            this(context, resolveDialogTheme(context, 0));
        }

这里没有传入一个主题资源,默认传入为0,调用两个参数的构造器。构造前通过resolveDialogTheme()
获取主题的id;

static int resolveDialogTheme(@NonNull Context context, @StyleRes int resid) {
        // Check to see if this resourceId has a valid package ID.
        if (((resid >>> 24) & 0x000000ff) >= 0x00000001) {   // start of real resource IDs.
            return resid;
        } else {
            TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true);
            return outValue.resourceId;
        }
    }

当传入了Application类作为context时,getTheme()后resolveAttribute()得到的值会放到outValue的resourceId中,这时获取到的resourceId为0;

当传入的是Activity作为context时,最终获取到的resourceId不是0;
这个resourceId 也会作为mTheme这个成员变量的值。

public Builder(@NonNull Context context, @StyleRes int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
            mTheme = themeResId;
        }
public AlertDialog create() {
            // We can't use Dialog's 3-arg constructor with the createThemeContextWrapper param,
            // so we always have to re-set the theme
            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

在create时,p将引用的context 以及mTheme的值传递给AlertDialog的构造器。

protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, resolveDialogTheme(context, themeResId));
        mAlert = new AlertController(getContext(), this, getWindow());
    }

AlertController构造时调用的getContext(),实际上获取到的是Dialog的Builder的context
因为AlertDialog构造函数调用了父类的构造函数,最终调用的是Dialog类的构造函数。
Dialog的getContext如下

public final @NonNull Context getContext() {
        return mContext;
    }

也就是初始构造时传入的context;

AlertController类是控制对话框生成的。

当使用本次使用的setItem方式构造对话框,它会使用context获取到一个listview的资源id。

final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
                R.attr.alertDialogStyle, 0);
...
        mListLayout = a.getResourceId(R.styleable.AlertDialog_listLayout, 0);

这里obtainStyledAttributes获取到一个系统属性集合。该方法源码如下:

/** * Retrieve styled attribute information in this Context's theme. See * {@link android.content.res.Resources.Theme#obtainStyledAttributes(int[])} * for more information. * * @see android.content.res.Resources.Theme#obtainStyledAttributes(int[]) */
    public final TypedArray obtainStyledAttributes(@StyleableRes int[] attrs) {
        return getTheme().obtainStyledAttributes(attrs);
    }

可以看出是通过getTheme的obtainStyledAttributes获取到的结果集。但是Application并没设置theme的方法,所以getTheme()无法获得theme,obtainStyledAttributes获取到的TypedArray 也无内容。从而也无法获取到mListLayout 。

最终会根据这个mListLayout 生成listview。

final RecycleListView listView =
                    (RecycleListView) mInflater.inflate(dialog.mListLayout, null);

这个listview就是最终在对话框中显示的内容。

当context为Application时,获取到的mListLayout 为0 ,不能inflate出listview。便报错。

最终改为当传入Activity作为context时,可以正常显示

public class EditProfileActivity extends BaseActivity {
    @Override
    protected int initLayoutRes() {
        return R.layout.activity_edit_my_profile;
    }

    @OnClick(R.id.btn_change_pic)
    void showChoose(){
        SimpleDialogUtils.showSimpleChooseDialog(this,R.array.pic_choose
                , new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {

            }
        });
    }
}
public class SimpleDialogUtils {

    public static void showSimpleChooseDialog(Context ctx ,int itemsId, DialogInterface.OnClickListener listener) {
        AlertDialog dialog = new AlertDialog
                .Builder(ctx)
                .setItems(itemsId, listener)
                .create();
        dialog.show();
    }
}

结论:Context的选择要慎重,并不是拿到了Application 的context就可以当万精油使用的。 涉及到View的context需要具体分析。

相关文章
相关标签/搜索