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