SQLite中Blob存储的序列化对象新增字段兼容更新方案
崩溃来自Java原生序列化的版本校验机制:实现Serializable接口的类如果没有显式定义serialVersionUID,JVM会根据类的成员结构自动生成版本号。3.2.2版本新增Phone字段后,类结构变化导致自动生成的版本号和老版本序列化数据里记录的版本号不匹配,反序列化时直接抛出InvalidClassException导致崩溃。
你仅新增1个字段、且类已经实现Serializable,不需要重构存储结构,按以下三步处理即可完全兼容老数据:
1. 固定类的序列化版本号,兼容老版本数据流
首先用3.2.1版本的Forum_Item类(仅包含Name、Email两个字段),通过以下代码拿到老版本自动生成的serialVersionUID值:
// 临时运行一次拿到值即可 long oldSerialVersion = ObjectStreamClass.lookup(Forum_Item.class).getSerialVersionUID();
将拿到的版本号硬编码到3.2.2版本的Forum_Item类中,固定版本号后,JVM不会再根据类结构自动生成版本号,新旧数据的版本校验可以通过。
2. 新增字段做反序列化兜底
老版本数据中不存在Phone字段,反序列化时该字段默认会被赋值为null(引用类型),如果业务上需要非空默认值,可以自定义readObject方法在反序列化时补全默认值,避免后续业务逻辑触发空指针。
修改后的Forum_Item类示例:
import java.io.Serializable; import java.io.IOException; import java.io.ObjectInputStream; public class Forum_Item implements Serializable { // 替换为你之前拿到的老版本serialVersionUID实际值 private static final long serialVersionUID = 替换为你算出来的实际版本号; private String Name; private String Email; // 新增字段 private String Phone; // 保留原有所有getter、setter、构造方法 // 反序列化兜底逻辑 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // 老数据反序列化后Phone为null,这里按业务需求赋默认值,比如空字符串 if (Phone == null) { Phone = ""; } } }
原理说明:Java序列化机制规定,只要类的
serialVersionUID一致,序列化流中缺少当前类定义的字段时,会自动给缺失字段赋类型默认值,不会抛出异常;流中多余的字段会被直接忽略,天然支持新增字段的兼容场景。
3. 执行存量数据迁移
在3.2.2版本首次启动时执行一次存量数据迁移,将老Blob数据反序列化为兼容后的新对象,再重新序列化存回数据库即可,迁移完成后打标记避免重复执行。
迁移代码示例:
private void migrateV322Data(Context context) { // 读取迁移标记,已经迁移过直接返回 SharedPreferences sp = context.getSharedPreferences("app_config", Context.MODE_PRIVATE); if (sp.getBoolean("migrate_3_2_2_done", false)) { return; } mDataBase db = new mDataBase(context); db.createDB(); db.beginTransaction(); try { // 查询所有存量Blob数据 Cursor cursor = db.ReadFromDB("select rowid, " + db.ColForumItem + " from " + db.forum_table_v2); if (cursor.moveToFirst()) { while (!cursor.isAfterLast()) { long rowId = cursor.getLong(0); byte[] oldBlob = cursor.getBlob(1); // 此时反序列化不会崩溃,Phone字段已被自动赋默认值 Forum_Item item = ByteToObject(oldBlob); // 序列化为新结构的字节数组 byte[] newBlob = ObjectToByte(item); // 更新回数据库 ContentValues cv = new ContentValues(); cv.put(db.ColForumItem, newBlob); db.update(db.forum_table_v2, cv, "rowid = ?", new String[]{String.valueOf(rowId)}); cursor.moveToNext(); } } cursor.close(); db.setTransactionSuccessful(); // 标记迁移完成 sp.edit().putBoolean("migrate_3_2_2_done", true).apply(); } catch (Exception e) { e.printStackTrace(); } finally { db.endTransaction(); db.close(); } }
- 计算老版本
serialVersionUID必须使用3.2.1版本未加Phone字段的Forum_Item类,用新类计算出的版本号和老数据不匹配,仍然会崩溃 - 不要修改原有
Name、Email字段的名称、类型,否则即使版本号一致也会反序列化失败 - 原有
ObjectToByte方法中创建的ByteArrayInputStream没有实际作用,可以直接删除减少冗余 - 迁移逻辑必须加完成标记,不要每次启动都执行,避免不必要的性能损耗
内容的提问来源于stack exchange,提问作者Sobhan

