Android中如何优雅解决序列化Drawable资源ID运行时失效问题?
问题背景
我正在开发一款食谱记录App,核心功能是保存和修改仅含食材列表及元数据的抽象食谱,帮助家人适配他们的思维模式。目前用GSON将Recipe对象序列化为JSON文件存储,读取时再反序列化为对象展示和修改。
但遇到一个问题:Ingredient类中存储的drawable资源ID(比如R.drawable.tomato),序列化到JSON后,再次读取使用时会抛出Resources$NotFoundException,因为Android运行时的资源ID可能会变化,之前序列化的ID已经无效。不想用switch-case逐个映射食材名称到资源ID,求程序化的优雅解决方案。
相关代码
Recipe.java
package me.eyrim.foodrecords2; import com.google.gson.annotations.SerializedName; public class Recipe { @SerializedName("recipe_name") private String recipeName; @SerializedName("recipe_desc") private String recipeDesc; @SerializedName("ingredients") private Ingredient[] ingredients; @SerializedName("recipe_id") private String recipeId; public String getRecipeName() { return this.recipeName; } public String getRecipeDesc() { return this.recipeDesc; } public Ingredient[] getIngredients() { return this.ingredients; } public String getRecipeId() { return this.recipeId; } }
Ingredient.java(原版本)
package me.eyrim.foodrecords2; import com.google.gson.annotations.SerializedName; public class Ingredient { @SerializedName("ingredient_name") private String ingredientName; @SerializedName("ingredient_drawable_tag") private int ingredientDrawableTag; public String getIngredientName() { return this.ingredientName; } public int getIngredientDrawableTag() { return this.ingredientDrawableTag; } }
生成的JSON示例
{ "recipe_name": "My test recipe 1 updated", "recipe_id": "0", "recipe_desc": "this is a test desc for my test recipe 1", "ingredients": [{ "ingredient_name": "Tomato", "ingredient_drawable_tag": 700003 }, { "ingredient_name": "Pepper", "ingredient_drawable_tag": 700002 } ] }
IngredientRecyclerViewAdapter.java(问题代码处已标记)
package me.eyrim.foodrecords2.recipeviewactivity; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import me.eyrim.foodrecords2.Ingredient; import me.eyrim.foodrecords2.R; public class IngredientRecyclerViewAdapter extends RecyclerView.Adapter<IngredientRecyclerViewAdapter.IngredientViewHolder> { private final Ingredient[] ingredients; private final Context context; public IngredientRecyclerViewAdapter(Ingredient[] ingredients, Context context) { this.ingredients = ingredients; this.context = context; } @NonNull @Override public IngredientViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.ingredient_row, parent, false); return new IngredientViewHolder(view); } @Override public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) { // 问题代码:序列化的资源ID已失效 holder.imageView.setImageResource(this.ingredients[position].getIngredientDrawableTag()); } @Override public int getItemCount() { return this.ingredients.length; } public class IngredientViewHolder extends RecyclerView.ViewHolder { private final ImageView imageView; public IngredientViewHolder(@NonNull View itemView) { super(itemView); imageView = itemView.findViewById(R.id.ingredient_image); } } }
运行时异常日志
E/im.foodrecords: Invalid ID 0x000aae63. D/AndroidRuntime: Shutting down VM E/AndroidRuntime: FATAL EXCEPTION: main Process: me.eyrim.foodrecords2, PID: 3368 android.content.res.Resources$NotFoundException: Resource ID #0xaae63 at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:225) at android.content.res.Resources.getValue(Resources.java:1428) at androidx.appcompat.widget.ResourceManagerInternal.createDrawableIfNeeded(ResourceManagerInternal.java:181) at androidx.appcompat.widget.ResourceManagerInternal.getDrawable(ResourceManagerInternal.java:146) at androidx.appcompat.widget.ResourceManagerInternal.getDrawable(ResourceManagerInternal.java:137) at androidx.appcompat.content.res.AppCompatResources.getDrawable(AppCompatResources.java:66) at androidx.appcompat.widget.AppCompatImageHelper.setImageResource(AppCompatImageHelper.java:91) at androidx.appcompat.widget.AppCompatImageView.setImageResource(AppCompatImageView.java:102) at me.eyrim.foodrecords2.recipeviewactivity.IngredientRecyclerViewAdapter.onBindViewHolder(IngredientRecyclerViewAdapter.java:35) ...(其余栈信息省略)
优雅解决方案
方案1:存储资源名称而非ID,运行时动态获取有效ID
Android的资源ID是编译时生成的,不同编译版本或运行环境可能变化,但资源名称是固定的。因此可以修改Ingredient类,存储drawable的资源名称(比如"tomato"),而非直接存ID。
修改Ingredient.java
package me.eyrim.foodrecords2; import com.google.gson.annotations.SerializedName; public class Ingredient { @SerializedName("ingredient_name") private String ingredientName; @SerializedName("ingredient_drawable_name") // 修改字段名 private String ingredientDrawableName; // 改为String类型存储资源名 public String getIngredientName() { return this.ingredientName; } public String getIngredientDrawableName() { return this.ingredientDrawableName; } }
对应的JSON结构修改
{ "recipe_name": "My test recipe 1 updated", "recipe_id": "0", "recipe_desc": "this is a test desc for my test recipe 1", "ingredients": [{ "ingredient_name": "Tomato", "ingredient_drawable_name": "tomato" // 存储资源名称 }, { "ingredient_name": "Pepper", "ingredient_drawable_name": "pepper" } ] }
修改Adapter的onBindViewHolder方法
使用getResources().getIdentifier()方法,根据资源名称、类型和包名获取当前有效的资源ID:
@Override public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) { Ingredient ingredient = ingredients[position]; // 获取资源ID:参数依次为资源名、资源类型(drawable)、应用包名 int drawableId = context.getResources().getIdentifier( ingredient.getIngredientDrawableName(), "drawable", context.getPackageName() ); // 检查ID是否有效,避免崩溃 if (drawableId != 0) { holder.imageView.setImageResource(drawableId); } else { // 设置默认占位图 holder.imageView.setImageResource(R.drawable.default_ingredient); } }
方案2:构建资源名称到ID的缓存映射(优化性能)
getIdentifier()方法性能略低,如果食材数量较多,建议提前构建一个HashMap缓存资源名称到ID的映射,避免每次调用getIdentifier():
缓存实现示例(可在Application或Adapter初始化时执行)
// 构建缓存 private static final HashMap<String, Integer> DRAWABLE_MAP = new HashMap<>(); public static void initDrawableMap(Context context) { // 添加所有食材对应的资源名和ID DRAWABLE_MAP.put("tomato", R.drawable.tomato); DRAWABLE_MAP.put("pepper", R.drawable.pepper); // 其他食材... }
Adapter中使用缓存
@Override public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) { Ingredient ingredient = ingredients[position]; Integer drawableId = DRAWABLE_MAP.get(ingredient.getIngredientDrawableName()); if (drawableId != null) { holder.imageView.setImageResource(drawableId); } else { holder.imageView.setImageResource(R.drawable.default_ingredient); } }
这种方式既避免了switch-case的繁琐,又比getIdentifier()性能更高,适合食材数量固定的场景。
方案3:将图片放在assets目录,直接按文件名加载
如果不想依赖资源ID,可以把食材图片放在assets目录下,存储图片文件名,然后通过AssetManager加载:
修改Ingredient.java存储文件名
@SerializedName("ingredient_drawable_file") private String ingredientDrawableFile; // 比如"tomato.png"
Adapter中加载图片
@Override public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) { Ingredient ingredient = ingredients[position]; try { // 从assets获取输入流 InputStream is = context.getAssets().open(ingredient.getIngredientDrawableFile()); Drawable drawable = Drawable.createFromStream(is, null); holder.imageView.setImageDrawable(drawable); } catch (IOException e) { e.printStackTrace(); holder.imageView.setImageResource(R.drawable.default_ingredient); } }
这种方式完全脱离Android资源系统的ID限制,适合图片较多或需要动态添加图片的场景,但注意assets目录下的文件结构要清晰,文件名要统一。
内容的提问来源于stack exchange,提问作者Eyrim

