背景:周一早上例行巡检,发现有一个crash出现两次,刚好新版本刚灰度10%比例。立即暂停灰度,确认问题影响范围。
一、问题表现
1. bugly上对应crash异常上报如下:
2.定位代码位置如下
代码有部分直接马赛克,非常简单直接的代码一个data对象,实现序列化接口,里面有定义序列号。成员变量set集合直接创建对象,并给默认初始值。提供一个get方法返回这个set.
并没有地方对这个对象赋值为null,照理说不应该存在为空的场景。 如果是自己遇到这个问题也可以看看自己分析思路、方向。
3.问题原因
上面的data对象做了本地持久化存储。set字段是新版本需求加的。历史版本有缓存一个对象在本地。覆盖升级安装时,缓存数据反序列化,因为老版本没有set字段,反序列化后 set会变成null.
这部分历史代码还没有用KT,所以外面在调用时,如果没有非空判断。就存在crash问题。
实际开发时候,多人协作时这种历史代码不是自己开发不了解它的使用逻辑时,很容易出现这种问题。而且测试不容易发现。因为启动时候会请求服务器数据更新缓存。出现crash用户刚好是启动阶段请求服务器接口慢,在使用到数据反序列化时还没有请求回来数据更新缓存出现。
我正常去某个对象加个字段,并且成员变量上来就new出来的对象怎么会空呢???
大部分使用场景都是直接new 对象使用,肯定不会出现空。但是如果有持久化存储这种序列化对象,在反序列化时候上面的代码就是可能为空,所以使用时注意判断
4.问题复现
安装老版本,抓包看到请求数据后,覆盖安装新版本,mock接口延迟返回。就能复现,异常和bugly一致。
5.问题修复
问题修复就很简单了,因为暂停了灰度,需要快速修复上线继续灰度放量,直接获取方法里面判断如果为空 创建对象返回。
后续修复有几个方向:1.反序列化时增加逻辑去兜底 开发者不用关注需要做这种兜底 2.增加一些探测机制
二、序列化反序列化知识点梳理
遇到这个问题时,除了去看历史逻辑不熟悉耗费时间,发现序列化反序列化相关很多知识点已经比较模糊了,整个问题确认验证比较占用时间。梳理下相关知识点巩固下,也可以自己看下下面的问题是不是都非常清楚。
1.反序列化后的对象会重新调用构造函数吗?
答:不会, 因为是从二进制直接解析出来的
2.序列化与反序列化后的对象是什么关系?
答:是一个深拷贝, 前后对象的引用地址不同
3.序列化ID是什么,有什么用?不用有什么影响?更改了有什么影响
serialVersionUID 是一个 private static final long 型 ID, 一般是对象的哈希码,可以使用 serialver 这个 JDK 工具来查看序列化对象的序列化ID。
用于对象的版本控制。也可以在类文件中指定 serialVersionUID。不指定serialVersionUID时,当你添加或修改类中的任何字段时,则已序列化类将无法恢复,因为为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化过程依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发InvalidclassException
4.针对反序列化场景新增加字段有没有方法可以规避出现空指针场景?
从上面的描述我们知道,如果缓存了一份老版本的数据在本地,新版本修改了字段,直接从老版本缓存反序列化数据使用,可能出现为空场景。大项目协作时,从架构上避免出现上述问题。可以采用一些补全的实现,比如:JSON。代码如下:
public class SPUtilFile {
private static final String PREF_NAME = "my_preferences";
public static void saveObject(Context context, String key, Serializable object) {
SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
String base64String = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT);
editor.putString(key, base64String);
editor.apply();
} catch (IOException e) {
e.printStackTrace();
}
}
public static <T extends Serializable> T getObject(Context context, String key, Class<T> clazz) {
SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String base64String = sharedPreferences.getString(key, null);
if (base64String == null) {
return null;
}
byte[] byteArray = Base64.decode(base64String, Base64.DEFAULT);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
T object = (T) objectInputStream.readObject();
objectInputStream.close();
return object;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
Log.d("MainActivity", "异常");
}
return null;
}
执行一样的操作,先缓存一份到本地,然后下次反序列数据,新增加的字段比如 HashSet<String> set = new HashSet(); 的set就会出现空,使用时候没注意就空指针了。 但是如果使用下面的方法,使用JSON转换,它会自动补全set反序列化出来是一个空数组,不是null。
public class SPUtil {
private static final String PREF_NAME = "my_preferences";
public static void saveObject(Context context, String key, Serializable object) {
SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
String jsonString = JSON.toJSONString(object);
editor.putString(key, jsonString);
editor.apply();
}
public static <T extends Serializable> T getObject(Context context, String key, Class<T> clazz) {
SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String jsonString = sharedPreferences.getString(key, null);
if (jsonString == null) {
return null;
}
return JSON.parseObject(jsonString, clazz);
}
}
5.序列化时如果某个成员变量不序列化怎么处理?反序列化后是什么样的?
如果你不希望任何字段是对象的状态的一部分,然后声明它静态或瞬态根据你的需要,这样就不会是在 Java 序列化过程中被包含
反序列化后对应字段是null,因为序列化时候根本没存进去
6.某个对象中有一个内部对象未实现序列化,序列化时候会出现什么
运行时将引发不可序列化异常 NotSerializableException
7.如果当前类是可序列化,但父类不是,父类的成员反序列化后如何
反序列化后为空
8.是否可以自定义序列化?怎么做
对于序列化一个对象需调用 ObjectOutputStream. writeObject(saveThisObject),并用ObjectInputStream. readObject()读取对象,但 Java虚拟机为你提供的还有一件事,是定义这两个方法。如果在类中定义这两种方法,则JVM将调用这两种方法,而不是应用默认序列化机制。
你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行
9.序列化源码看过吗?主要用到哪些,枚举特殊吗?
readOb jectO 的用法、vriteObject O、readExternal () 和 writeExternal0•Java 序列化由java.io. ObjectOutputStream类完成。该类是一个筛选器流,它封装在较低级别的字节流中,以处理序列化机制。要通过序列化机制存储任何对象,我们调用
ObjectOutputStream. writeObject(savethisobject),并反序列化该对象,我们称之为ObjectInputStream.readObjectO方法。调用以 writeObject( 方法在 java 中触发序列化过程。
关于 readobject()方法,需要注意的一点很重要一点是,它用于从持久性读取字节,并从这些字节创建对象,并返回一个对象,该对象需要类型强制转换为正确的类型