工作中需要在Android平台与嵌入式设备通信,互联网环境中数据交互一般采用json格式,解析比较容易。嵌入式设备间通信常常采用字节流传输,通信过程中需要逐个字节、甚至逐个位去读取,非常麻烦。
Java语言与字节编码互相转换的框架实际上是有的,比如说Javolution,可以把C语言的结构体转换为Java类,但是国内用户很少,文档稀少,转换某些结构有bug,难以解决,于是打算自己写一个字节序列化框架。

思路

嵌入式设备通信过程中的字节数据,实际上代表的就是嵌入式程序中的C语言结构体;解析这些字节数据,实际上就是把c语言结构体对应的内存数据翻译成java对象。
字节数据可以逐个解析,c语言中的一个类型可能对应一个或多个字节,定义类型对象PType类用来描述C语言中的单字节数据、多字节数据、字符串、字节数组等,定义PStruct类来描述一组通信数据,一个PStruct可以关联多个PType。

主要结构

paste image
PStruct是抽象类,按照字节通信协议的格式编写对应的Struct类,例如:

1
2
3
4
5
6
7
8
public class StructA extends PStruct {
public PInt model = Int(1);
public PLong value = Long(5);
public PString name = Str(8);
public PByte bytes = Byte(5);
public StructB structB = new StructB();
public StructB[] arrays = array(new StructB(), 6);
}

其中的Int()、Long()、Str()、Byte()方法定义在PStruct父类,用来初始化相应的类型,StructA类实例化时会自动初始化这些变量,例如:

1
2
3
4
5
6
protected PInt Int(int length) {
PInt pInt = new PInt(length);
fieldInfos.add(new FieldInfo(pInt, fieldInfos.size() + 1, length));
this.capacity += length;
return pInt;
}

PStruct在初始化时将所有的字段属性例如类型、所占字节长度,顺序等,保存到fieldInfos,fieldInfos是线性单向列表,序列化和反序列化时会遍历fieldInfos。这么设计的优点是可以避免使用反射,提高性能。

难点

实际业务中,通信数据里难免回出现大量的循环结构的数据,例如一些参数设置、历史记录信息等。采由初始化变量的方式,使得PStruct可以天然支持嵌套定义,因此上述代码StructA里的StructB可以正常初始化。对于线性结构,这里提供了array()方法,用来初始化数组对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected <T extends Parsable> T[] array(T object, int size) {
removeField(object);
T[] array = (T[]) Array.newInstance(object.getClass(), size);
array[0] = object;
if (object instanceof PType) {
PType type = (PType) object;
for (int i = 1; i < array.length; i++) {
array[i] = (T) type.copyEmpty();
}
} else if (object instanceof PStruct) {
for (int i = 1; i < array.length; i++) {
try {
array[i] = (T) object.getClass().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
fieldInfos.add(new FieldInfo(array, fieldInfos.size() + 1, -1));
this.capacity += array.length * object.length();
return array;
}

array()方法的作用是初始化指定长度的数组,并且初始化数组中的所有元素,这里用到了反射(newInstance()方法),没有使用java的Clone机制,是因为必须要深拷贝,而PStruct是父类,无法获取子类的属性信息,只能用反射,反射执行构造方法显然比反射读取类的声明变量要快得多。

上下文(PSturctContext)

因为存在嵌套,必须要使用上下文来保存解析数据的源数据、目标数据、数据长度等信息,PStruct和PType都实现了Parsable接口,Parsable接口的deserialize()、serialize()方法分别用来解析和序列化数据,这两个方法都引用了PStructContext参数,一个PStruct和其子Struct以及所有Type字段共享一个PStructContext。
serialize方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void serialize(PStructContext structContext) {     
for (FieldInfo fieldInfo : fieldInfos) {
if (fieldInfo.target.getClass().isArray()) {
Parsable[] array = (Parsable[]) fieldInfo.target;
for (Parsable parsable : array) {
parsable.serialize(structContext);
}
} else {
Parsable parsable = (Parsable) fieldInfo.target;
parsable.serialize(structContext);
}
}
}

deserialize方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deserialize(PStructContext structContext) {
for (FieldInfo fieldInfo : fieldInfos) {
if (fieldInfo.target.getClass().isArray()) {
Parsable[] array = (Parsable[]) fieldInfo.target;
for (Parsable parsable : array) {
parsable.deserialize(structContext);
}
} else {
Parsable parsable = (Parsable) fieldInfo.target;
parsable.deserialize(structContext);
}
}
}

总结

本文实现一种轻量的字节序列化框架,具有以下特点:

  • 由于主要的解析过程不涉及反射,因此性能很高,经测试,同样的Struct类,解析为字节码比将该类用Fastjson解析为json字符串要快100倍。
  • 结构化很好,只需要按协议格式编写Struct类,使用方便,易于理解。
  • 字段类型易于扩展,实现PType即可,PInt、PLong、PByte、PString提供了大量实用的工具方法,例如位操作、按位取值等。