22、复杂数据存储:JSON字段存储、Bitmap图片存储(BLOB)、文件路径存储策略、对象序列化与反序列化、Gson/Jackson与数据库结合
说实话,做Android开发这么多年,我见过太多人在「复杂数据存储」上栽跟头了。
SQLite本身是个关系型数据库,存个整数、字符串啥的挺顺手。但一旦遇到JSON对象、图片、或者一个复杂的Java对象,很多人就开始「硬塞」——要么拆成几十个字段,要么干脆不用数据库了。
其实没必要。SQLite虽然轻量,但对付这些复杂场景,它是有「正规军」方案的。今天我就把这几招全盘托出。
核心观点:复杂数据存储的本质,是「序列化与反序列化」的问题。无论JSON、Bitmap还是对象,最终都要变成数据库能理解的「字节流」或「字符串」。
一、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别手写序列化。这样你的数据库既轻量又灵活。