1. 程式人生 > >安卓 AlertDialog 報 android.content.res.Resources$NotFoundException 的坑

安卓 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需要具體分析。