问题来源
最近一次项目的版本开发中,产品经理希望新增加一些场景功能,这时我们原本数据结构DeskPushDetailsBean的字段不够用,只能新增加字段tempDescribe,开发完成后调试功能满足预期,ok,发版了灰度上线,一天后从友盟统计平台看到某个用户手机上5分钟内连续报错了将近8次(估计用户得卸载我们app了),手机型号:OPPO A83 (android 7.1.1),详细报错为:
--logversion:utracea
Process Name: 'com.xxx.weather'
Thread Name: 'main'
Back traces starts.
java.lang.RuntimeException: Unable to start activity ComponentInfo{XXXBottomViewActivity}: java.lang.RuntimeException: Parcelable encountered IOException reading a Serializable object (name = com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2977)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3101)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1737)
at f.j.a.a.a.handleMessage(ActivityThreadCallback.java:4)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:232)
at android.app.ActivityThread.main(ActivityThread.java:6914)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1103)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964)
Caused by: java.lang.RuntimeException: Parcelable encountered IOException reading a Serializable object (name = com.xxxkit.bean.DeskPushDetailsBean)
at android.os.Parcel.readSerializable(Parcel.java:2650)
at android.os.Parcel.readValue(Parcel.java:2440)
at android.os.Parcel.readArrayMapInternal(Parcel.java:2756)
at android.os.BaseBundle.unparcel(BaseBundle.java:269)
at android.os.BaseBundle.getBoolean(BaseBundle.java:731)
at android.content.Intent.getBooleanExtra(Intent.java:6155)
at com.jess.arms.integration.ActivityLifecycle.onActivityCreated(ActivityLifecycle.java:2)
at android.app.Application.dispatchActivityCreated(Application.java:206)
at android.app.Activity.onCreate(Activity.java:1041)
at androidx.core.app.ComponentActivity.onCreate(ComponentActivity.java:1)
at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:1)
at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:16)
at com.xxx.XXXBottomViewActivity.onCreate(XXXBottomViewActivity.java:3)
at android.app.Activity.performCreate(Activity.java:6987)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2925)
... 10 more
Caused by: java.io.InvalidClassException: com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:606)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at android.os.Parcel.readSerializable(Parcel.java:2648)
... 26 more
其中重要的关键报错信息在第2个Caused by,即java.io.InvalidClassException: com.xiaoniu.deskpushuikit.bean.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083;
原因分析
打开项目,查看DeskPushDetailsBean类;
public class DeskPushDetailsBean implements Serializable {
public DayWeatherBean today;
public DayWeatherBean tomorrow;
public MinutelyBean minutely;
public NewsBean news;
public WarningBean alert;
public RealWeatherBean realTime;
public CustomTopBean customTopBean;
public String pushType;
public boolean showCloseButton;
public String triggerTime;
public TemputerTextBean tempDescribe;//新增属性
}
发现它序列化时implements Serializable,点击走进Serializable源码或者查看文档能不能发现一些线索,我们可以看见下面一段注释备注:
If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes. Android implementation of serialVersionUID computation will change slightly for some classes if you're targeting android N. In order to preserve compatibility, this change is only enabled is the application target SDK version is set to 24 or higher. It is highly recommended to use an explicit serialVersionUID field to avoid compatibility issues.
简单翻译一下(英文很差、凑合着看):
如果一个可序列化的类没有明确指定serialVersionUID,那么它将如同Java规范文档中描述那样,序列化运行时将基于这个类的各个方面计算默认的serialVersionUID值。然后,强烈推荐所有实现serializable的序列化类明确的指定serialVersionUID值,因为默认serialVersionUID值的计算对类的细节高度敏感,可能取决于编译器的实现而改变,因此在反序列化过程中将导致出乎意料的InvalidClassException,因此,要保证不同java编译器实现之间的serialVersionUID值一致,可序列化的类必须声明一个明确的serialVersionUID值。强烈建议在可能的情况下使用private修饰符显式的声明serialVersionUID,因为这样的声明只适用于当前直接声明的类——serialVersionUID字段作为子类继承的成员是没有用的。数组类无法显式的声明serialVersionUID,因此它们始终具有默认的计算值,但是数组类不需要匹配serialVersionUID值。
看到这里,问题很明显了,我们在原来类中增加了tempDescribe变量,但是一直并没有明确指定serialVersionUID值,事实上,新的变量会导致编译器运行时会为它自动生成新的serialVersionUID值,这个值和原来自动生成的serialVersionUID值不一致;
生成serialVersionUID值
AndroidStudio可以很方便的增加serialVersionUID值,设置如下:
File->Settings->Editor->Inspections,输入serializable搜索,找到Serializable class without ‘serialVersionUID’项,勾选apply,ok即可;
image.png然后找到需要生成serialVersionUID值的目标类,双击类文件中类名,左侧会出来一个小灯泡,点击“Add ‘serialVersionUID’ field,编译器就自动帮我们生成好了serialVersionUID值;
image.png生成完后的如下,细心的同学应该已经发现了,这里生成的值就是开始报错异常InvalidClassException后面的local class serialVersionUID = 8478373576351162083:
image.png需要注意的是:不管是增加、减少成员变量个数、或者改变成员变量的名字,都会导致自动生成serialVersionUID值的改变;
解决方案
根据文档,既然我们要保持serialVersionUID的值不变,那么就不能用最新的增加tempDescribe变量后的代码生成serialVersionUID值,而是要用之前的代码生成,通过git历史记录找到之前的旧代码,再重新生成则得到 private static final long serialVersionUID = 6163620063913001067L;这个值也正好是开始报错异常InvalidClassException里面的前部分local class incompatible: stream classdesc serialVersionUID = 6163620063913001067中的值,至此,我们只需要在最新代码中增加下面一行即可
private static final long serialVersionUID = 6163620063913001067L;
其实查看《阿里巴巴Java开发手册终极版v1.3.0》文档,里面 OOP 规约的第10条也提到了
10\. 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。
注:这样其实也存在一个问题,我们为了兼容只考虑了老版本用户的serialVersionUID是6163620063913001067L,但是已经增加tempDescribe变量的灰度用户的serialVersionUID还是被自动生成为8478373576351162083L,所以当这部分灰度用户升级到最新修复的全量版本时,还是会出现InvalidClassException,报错为:
Caused by: java.io.InvalidClassException: com.xxx.DeskPushDetailsBean; local class incompatible: stream classdesc serialVersionUID = 6163620063913001067, local class serialVersionUID = 8478373576351162083
这时只能在反序列化的地方try catch捕获一下了。
综上,解决办法就是:保持serialVersionUID的值不变 + 反序列化的地方try catch捕获
验证
编写测试类SerializableDemo1.java,将DeskPushDetailsBean的值序列化(文件输出流)保存到当前工程的tempFile文件中,再通过SerializableDemo2从tempFile文件中读取(反序列化过程)出来,看是否会抛出异常;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author
* @since 2020/12/27 16:42
*/
public class SerializableDemo1 {
public static void main(String[] args) {
DeskPushDetailsBean deskPushDetailsBean = new DeskPushDetailsBean();
deskPushDetailsBean.pushType = "xzbTest";
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
objectOutputStream.writeObject(deskPushDetailsBean);
} catch (IOException e) {
e.printStackTrace();
}finally {
// close;
}
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
* @author
* @since 2020/12/27 17:08
*/
public class SerializableDemo2 {
public static void main(String[] args) {
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
DeskPushDetailsBean deskPushDetailsBean = (DeskPushDetailsBean) ois.readObject();
System.out.println(deskPushDetailsBean);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
// close;
}
}
}
执行SerializableDemo2类main方法,发现正常打印deskPushDetailsBean对象值,说明反序列化成功;
image.png任意更改serialVersionUID为其它值后验证,发现都会导致反序列化失败,抛出InvalidClassException异常,同学们可自行验证一下。
----------------------------------------结束分割线-----------------------------------
网友评论