最近项目进行了一次target sdk的升级28版本的改造,在处理了一些target 版本的Android9.0的兼容之后,项目整体运行起来没有什么问题,但在之后作为SDK给另一个项目使用之后出现了一个比较罕见的问题-NotSerializableException: com.google.gson.internal.StringMap数据序列化问题,看似比较简单,也是困扰了很久,下面总结一下针对这个问题的跟踪查询。
错误日志一、问题描述
我在进行功能验证的过程中发现了一个比较奇怪的问题,当我在页面A的时候数据没有任何问题,也可以正常显示,但是不管我从页面A(Fragment)跳转到任何页面B、C、D(Activity,Fragment都行)都会崩溃,控制台输出问题截图上面的的异常信息。然而我从其他页面E(Fragment)跳转到B、C、D的时候都没有任何问题。
根据控制台的异常日志输出信息可以看出这就是一个简单的序列化问题,刚开始我是这么认为的,日志信息明确说明了是PageBean的写入数列化问题,那我们就找PageBean然后实现序列化就可以了。当我找到PageBean的时候就懵了 public class PageBeanimplements Serializable {},明明都已经实现了Serializable 接口了,为什么还是报错了,是不是子类没有实现Serializable 接口呢!然后我仔细检查了PageBean的每个子类以及子类的子类,全部都实现了Serializable 接口,问题开始变得复杂了。回头又看错误日志,发现还有一个信息,就是StringMap这个类,但是我搜索了一个引用的gson库并没有找个这个文件。
二、问题定位
我把问题同步给了项目leader,经过leader跟同事的连夜查找,总算是大概定位到了问题所在,而且也找到了StringMap这个类。那天我早早的可耻的溜了,事后也是感到非常的惭愧。原来StringMap是在早些的gson库里所存在帮助json数据解析的类,而我们的项目的gson比较新,所以一直找不到这个类,我们的另一个项目是早些的gson库,所以打包后到另一个项目才会出现此问题。出问题的地方大概在下面所存在的写入序列化对象的代码中
public static TemplateContainerFragment newInstance(ChannelNavBean channelNavBean, PageBean firstPageBean) {
TemplateContainerFragment vesselFragment = new TemplateContainerFragment();
Bundle bundle = new Bundle();
bundle.putSerializable(AppParams.INTENT_PARAM_CHANNEL_NAV_BEAN, channelNavBean);
if (firstPageBean != null) {
bundle.putSerializable(AppParams.INTENT_PARAM_CHANNEL_PAGE_BEAN, firstPageBean);
}
vesselFragment.setArguments(bundle);
return vesselFragment;
}
大概定到问题以后,leader为了锻炼我解决问题的能力,也是抛给我2个问题,希望我能多提高自己解决问题的能力
1.序列化是在什么时候出问题的?出问题的序列化对象在什么位置上?SrtingMap对象在当中扮演的角色是什么?
2.为什么页面初始化的时候没有问题,反而在进入下一级页面的时候出现崩溃?
三、问题分析
根据日志信息和已找到的代码可以大概确定PageBean是在序列化的时候出现了问题,我找到上面的相关代码进行debug验证,寻找PageBean中未实现序列化的StringMap对象,还真的有所发现
产生StringMap的PageBean为什么PageBean里面会有StringMap对象?带着这个疑问我跟踪查找了一个StringMap的产生,终于在gson库里面的ObjectTypeAdapter对象中找到了StringMap的产生,原来是在数据解析的时候如果有JSONArray有未知的List<Object> list,Object会被转化成为一个StringMap对象存储,而StringMap是没有序列化的对象,所以在传递数据的过程中会出现异常。那么第一个问题就找到了答案
public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch(token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList();
in.beginArray();
while(in.hasNext()) {
list.add(this.read(in));
}
in.endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new StringMap();
in.beginObject();
while(in.hasNext()) {
map.put(in.nextName(), this.read(in));
}
in.endObject();
return map;
case STRING:
return in.nextString();
case NUMBER:
return in.nextDouble();
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new IllegalStateException();
}
}
剩下的问题就是这个方法明明是在页面初始化的时候调用的,为什么在页面初始化的时候没有问题,返回再页面进入下一级页面的时候回出现崩溃?其实发现进入下一个页面的时候出现问题,也就发现了思路,进入下一级页面的时候上一个页面要保存数据,对,就是保存数据的时候可能会出现问题,然后我就跟踪onSaveInstanceState(@NonNull Bundle outState)的方法,页面离开确实调用了该方法,但是outState参数是空的,没有传递任何数据,为什么会出问题呢,经过一系列的跟踪,终于在FragmentState中发现了问题,原来在fragment页面
Parcel类的writeBundle bundle类的writeToParcel writeToParcelInner writeArrayMapInternal@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mClassName);
dest.writeInt(mIndex);
dest.writeInt(mFromLayout ? 1 : 0);
dest.writeInt(mFragmentId);
dest.writeInt(mContainerId);
dest.writeString(mTag);
dest.writeInt(mRetainInstance ? 1 : 0);
dest.writeInt(mDetached ? 1 : 0);
dest.writeBundle(mArguments);
dest.writeInt(mHidden ? 1 : 0);
dest.writeBundle(mSavedFragmentState);
}
for循环写入数据
writeValue写入数据,原来这里要求写入的数据对象以及子对象都必须是序列化的数据,否则就会出现异常。为什么页面初始化进入写入的数据没有问题,反而在页面离开保存数据的时候写入的数据会出现异常?带着这个疑问我又看了一下bundle.putSerializable()这个方法。
Bundle的putSerializable方法 putSerializable看到代码之后我释然了,原来putSerializable这个方法值值要求传入的对象被序列化就可以,并不要求子对象必须都实现数列化接口。所以才导致页面初始化的时候并没有问题,反而在页面离开保存数据的时候写入数据异常。
四、问题跟踪解决
由于我们的数据PageBean的解析里面JSONArray出现的问题,所以就尝试在不改变gson库版本号的同时解决这个问题,于是根据返回的数据结构尝试把JSONAarray替换成List<JSONObject>形式,于是又做了一番兼容的尝试工作,但是由于我们的数据结构问题,并没有成功,最后还是报出了相同的错误,经过查看还JSONObject中还是出现了没有序列化的StringMap。
仍然有问题的PageBean最后只能沟通把另一个项目的gson库进行升级解决这个问题。
网友评论