22、复杂数据存储:JSON字段存储、Bitmap图片存储(BLOB)、文件路径存储策略、对象序列化与反序列化、Gson/Jackson与数据库结合

说实话,做Android开发这么多年,我见过太多人在「复杂数据存储」上栽跟头了。

SQLite本身是个关系型数据库,存个整数、字符串啥的挺顺手。但一旦遇到JSON对象、图片、或者一个复杂的Java对象,很多人就开始「硬塞」——要么拆成几十个字段,要么干脆不用数据库了。

其实没必要。SQLite虽然轻量,但对付这些复杂场景,它是有「正规军」方案的。今天我就把这几招全盘托出。

核心观点:复杂数据存储的本质,是「序列化与反序列化」的问题。无论JSON、Bitmap还是对象,最终都要变成数据库能理解的「字节流」或「字符串」。

复杂数据存储策略总览 JSON字段存储 Bitmap图片(BLOB) 文件路径存储 对象序列化 TEXT类型存储 BLOB类型存储 TEXT存储路径 BLOB/序列化流 Gson/Jackson序列化 Bitmap→byte[] File路径→String Serializable/Parcelable 最终落地:SQLite 的 TEXT / BLOB 字段

一、JSON字段存储:把「对象」变成「字符串」

JSON存储,说白了就是把一个Java对象转成JSON字符串,然后塞进TEXT字段里。

我早期做项目时,有个用户配置表,字段多达30多个。每次加需求都要改表结构,烦得要死。后来我改成存一个JSON字段,世界清净了。

我的习惯:对于「配置类」「扩展属性类」的数据,优先考虑JSON字段。但如果是需要频繁查询、排序的字段,还是老老实实拆成独立列。

举个例子,一个用户可能有多个地址,用JSON字段存就很合适:

// 实体类
public class User {
    private int id;
    private String name;
    private String addressesJson; // 存JSON字符串
}

// 地址列表
public class Address {
    private String city;
    private String street;
    private String zipCode;
}

// 存储时
List<Address> addresses = new ArrayList<>();
addresses.add(new Address("北京", "朝阳区xxx", "100000"));
String json = new Gson().toJson(addresses);
user.setAddressesJson(json);

// 读取时
String json = cursor.getString(cursor.getColumnIndex("addresses_json"));
List<Address> addresses = new Gson().fromJson(json, 
    new TypeToken<List<Address>>(){}.getType());

注意:JSON字段虽然灵活,但SQLite不支持对JSON内部字段直接建立索引。如果你需要按「城市」查询用户,那还是拆列吧。

二、Bitmap图片存储(BLOB):能存,但别乱存

Bitmap存数据库?技术上完全可行。SQLite的BLOB类型就是干这个的。

但我得说句实话:我强烈不建议你把大图片直接塞进数据库。为什么?

  • 数据库文件会迅速膨胀,备份、迁移都痛苦
  • 读写性能下降,尤其是多张图片时
  • 内存开销大,Bitmap转byte[]再转回来,很吃内存

那什么时候用BLOB存图片?我个人经验是:小图标、缩略图、头像(小于100KB)。大图还是走文件路径策略。

// Bitmap转byte[]
public byte[] bitmapToBytes(Bitmap bitmap) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
    return baos.toByteArray();
}

// byte[]转Bitmap
public Bitmap bytesToBitmap(byte[] bytes) {
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}

// 存储到数据库
ContentValues values = new ContentValues();
values.put("avatar", bitmapToBytes(bitmap));
db.insert("user", null, values);

// 读取
byte[] bytes = cursor.getBlob(cursor.getColumnIndex("avatar"));
Bitmap bitmap = bytesToBitmap(bytes);

避坑指南:我曾经把一个2MB的图片存进BLOB,结果数据库文件从10MB暴涨到200MB,查询速度直接掉到秒级。后来改成存文件路径,数据库只有几十KB,图片用Glide加载,又快又稳。

三、文件路径存储策略:最推荐的方案

对于图片、音频、视频这类大文件,我的建议是:文件存磁盘,路径存数据库

这样做的好处很明显:

  • 数据库保持轻量,只存字符串
  • 文件可以单独管理,方便备份、删除
  • 加载时可以用Glide、Picasso等框架,自带缓存和缩放
// 存储文件路径
public void saveImagePath(long userId, String filePath) {
    ContentValues values = new ContentValues();
    values.put("user_id", userId);
    values.put("image_path", filePath);
    db.insert("user_images", null, values);
}

// 读取路径,然后用Glide加载
String path = cursor.getString(cursor.getColumnIndex("image_path"));
ImageView imageView = findViewById(R.id.avatar);
Glide.with(this)
     .load(new File(path))
     .into(imageView);

小技巧:文件路径建议用相对路径,不要写死绝对路径。比如用 context.getFilesDir() + "/images/" + fileName,这样即使应用数据被清理,路径也不会失效。

四、对象序列化与反序列化:Serializable vs Parcelable

如果你想把一个完整的Java对象存进数据库,除了JSON,还可以用Java自带的序列化机制。

这里有两个选择:

特性 Serializable Parcelable
实现方式 实现接口即可,简单 需要手动编写,稍复杂
性能 较慢(反射机制) 快(手动编码)
适用场景 少量数据、临时存储 大量数据、跨进程通信
数据库存储 可以,但效率一般 推荐,效率高

我个人习惯:如果只是存数据库,用Serializable就够了。但如果你要跨进程传递(比如Intent传对象),那必须用Parcelable。

// Serializable方式
public class UserConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    private boolean darkMode;
    private int fontSize;
    private String language;
}

// 序列化存储
UserConfig config = new UserConfig(true, 16, "zh");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(config);
byte[] bytes = bos.toByteArray();

ContentValues values = new ContentValues();
values.put("config", bytes);
db.insert("user_settings", null, values);

// 反序列化读取
byte[] bytes = cursor.getBlob(cursor.getColumnIndex("config"));
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
UserConfig config = (UserConfig) ois.readObject();

注意:序列化后的对象如果修改了类结构(增删字段),反序列化会失败。记得维护好 serialVersionUID

五、Gson/Jackson与数据库结合:现代Android的标配

说实话,现在很少有人手动写序列化了。Gson和Jackson才是主流。

这两者选哪个?我个人的经验:

  • Gson:轻量、简单、够用。适合中小型项目。
  • Jackson:功能强大、性能更好、注解丰富。适合大型项目。

嗯,我大部分项目用Gson,因为够简单。但如果你对性能有极致要求,或者需要复杂的自定义序列化,Jackson是更好的选择。

// 用Gson存复杂对象
public class Order {
    private int orderId;
    private List<OrderItem> items;
    private Address shippingAddress;
}

// 存储
Order order = new Order(1001, items, address);
String json = new Gson().toJson(order);
ContentValues values = new ContentValues();
values.put("order_data", json);
db.insert("orders", null, values);

// 读取
String json = cursor.getString(cursor.getColumnIndex("order_data"));
Order order = new Gson().fromJson(json, Order.class);

// 用Jackson(需要引入jackson-databind)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(order);
Order order = mapper.readValue(json, Order.class);

我的建议:在数据库层封装一个「序列化工具类」,统一管理Gson/Jackson的实例。这样切换库时只需要改一个地方。

public class DbSerializer {
    private static final Gson gson = new GsonBuilder()
        .setDateFormat("yyyy-MM-dd HH:mm:ss")
        .create();
    
    public static <T> String toJson(T obj) {
        return gson.toJson(obj);
    }
    
    public static <T> T fromJson(String json, Class<T> clazz) {
        return gson.fromJson(json, clazz);
    }
    
    public static <T> T fromJson(String json, Type type) {
        return gson.fromJson(json, type);
    }
}

好了,这五种策略基本覆盖了Android开发中90%的复杂数据存储场景。你想想看,无论是配置、图片、文件还是复杂对象,都能找到对应的方案。

记住一个原则:能用路径别存BLOB,能用JSON别拆表,能用Gson别手写序列化。这样你的数据库既轻量又灵活。


公众号:蓝海资料掘金营,微信deep3321