You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中如何优雅解决序列化Drawable资源ID运行时失效问题?

Android资源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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 23:15:36