20220902-记录若干类型在Android上序列化的方法
近期一项工作涉及把复杂内存数据跨进程、跨设备传输。借此机会梳理了若干常用类型在Android上实现序列化和互通的方法。
本文所讨论的序列化,是指数据类型和数据结构与byte[]
的相互转换(序列化&反序列化)。一个类型要能直接或间接的转换为byte[]
,才算满足了跨进程和跨端通信的必要条件。
本文不讨论基于Serializable
的序列化方法。本文只讨论若干具有代表性、通用性的类型在Android上序列化的方法。Java内置内型、其他各类数据结构,都可以通过转换为本文所述的类型、或用本文所述数据容器封装,来获得序列化能力。
String
首先来说String
。虽然很多场合把String
直接视为可直接收发的数据类型,但实际最终要转换为byte[]
才能在进程间拷贝、在网络上传输。String
和byte[]
的相互转换要考虑编码的问题,下面分两种场景说明他们互转的方法:
- 输入
String
,要序列化为byte[]
; - 输入
byte[]
,要序列化为String
;
把String序列化为byte[]
String
自带了序列化为byte[]
以及从byte[]
反序列化的方法,这不讨论加解密或压缩算法等复杂算法。在执行序列化或反序列化的时候需要指定编解码的Codec,如UTF-8
、UTF-16BE
、UTF-16LE
等,这里仅以网络传输常用的UTF-8
为例。
@Test
public void 验证把String序列化为bytes_并反序列化为正确的String() throws UnsupportedEncodingException {
String text = "testString中文";
byte[] bytes = text.getBytes("UTF-8");
println("bytes=" + deepToString(bytes));
String result = new String(bytes, "UTF-8");
println("result=" + result);
Assert.assertEquals(text, result);
}
用例运行结果:
bytes=[74,65,73,74,53,74,72,69,6e,67,e4,b8,ad,e6,96,87]
result=testString中文
把byte[]序列化为String
存在一些场合,输入的数据是任意的byte[]
,但需要序列化为String
:
- 打印到日志做人肉分析
- 存储到
SharedPreference
、xml、dababase等 - 存入JSON、Yaml等对可读性有需求的数据格式
一种典型方法是用Base64
做编解码,相比String.format("%02x", b)
而言可以节省字符串长度。
import android.util.Base64;
public static String encodeToString(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.DEFAULT);
}
public static byte[] decodeToBytes(String s) {
return Base64.decode(s, Base64.DEFAULT);
}
单测用例:
static String deepToString(byte[] bytes) {
Byte[] array = new Byte[bytes.length];
for (int i = 0; i < bytes.length; ++i) {
array[i] = bytes[i];
}
return "[" + Arrays.stream(array).map(b -> String.format("%02x", b)).collect(Collectors.joining(",")) + "]";
}
@Test
public void 验证bytes通过Base64与String序列化和反序列化的可用性() {
byte[] bytes = {0x11, 0x55, (byte) 0x99, (byte) 0xaa, (byte) 0xee, (byte) 0xff};
System.out.println("bytes=" + deepToString(bytes));
String str = encodeToString(bytes);
System.out.println("base64=" + str);
byte[] result = decodeToBytes(str);
System.out.println("result=" + deepToString(result));
Assert.assertArrayEquals(bytes, result);
}
运行结果:
bytes=[11,55,99,aa,ee,ff]
base64=EVWZqu7/
result=[11,55,99,aa,ee,ff]
Parcel系列:Parcel、Parcelable、Bundle
Android的IPC(如四大组件相关调用)都以Bundle
为数据容器,容纳其他可序列化的数据类型。Bundle
实现了Parcelable
,Parcelable
定义了写入数据到Parcel
和从Parcel
恢复数据的方法,最后Parcel
负责把数据序列化为byte[]
或从byte[]
反序列化。
把Parcel序列化为byte[]
序列化:
Parcel parcel = Parcel.obtain();
// TODO: write something into parcel here
byte[] bytes = parcel.marshall();
parcel.recycle();
// TODO: now save or send bytes
反序列化:
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
// TODO: read something from parcel here
parcel.recycle();
这里有一点注意:在unmarshall()
之后、从parcel
读数据之前,一定要调用parcel.setDataPosition(0)
,否则可能任何数据都读不出来。
把Parcelable转换为Parcel
方法1:
// 写入
parcel.writeParcelable(parcelable, flags);
// 读出
Parcelable parcelable = parcel.readParcelable(classLoader);
方法2:
// 写入
parcelable.writeToParcel(parcel, flags);
// 读出
Parcelable parcelable = T.CREATOR.createFromParcel(parcel);
需要注意的是:上述两套方法不能交叉使用,否则无法从Parcel
正确读出Parcelable
对象或抛出ClassNotFoundException
异常。根据Parcel
的源码,方法1复用了方法2的实现,并且额外写入了Parcelable
对象的真实类名。在读出的时候,会根据自定义对象的类名,反射寻找CREATOR
;如果CREATOR
不存在,则会让读出过程失败。
把Bundle转换为Parcel
Bundle
实现了Parcelabl
接口,可以以普通的Parcelable
身份写入Parcel
或从Parcel
读出。Parcel
为Bundle
实现了额外一套接口,相比Parcelable
提供了不同的实现。这里介绍在Parcelable
之外、Bundle
类特有的读写方法。
// 写入
parcel.writeBundle(bundle);
// 读出方法1
bundle = parcel.readBundle();
// 读出方法2
bundle.readFromParcel(parcel);
// 读出方法3
Bundle.CREATOR.createFromParcel(parcel);
由于Android为Bundle
提供了特殊照顾,推测Bundle
和Parcel
的各个转换方法可以混用。该推测经过初步测试验证通过,但笔者并未做完备的测试、也未透彻的分析过源码,本结论仅供参考。
用例演示
部分用例代码如下:
void 实验Bundle的转换一致性(Function<Bundle, Bundle> transform) {
String key = "key", value = "value";
Bundle bundle1 = new Bundle();
bundle1.putString(key, value);
println("bundle1=" + bundle1);
Bundle bundle2 = transform.apply(bundle1);
String result = bundle2.getString(key, null);
println("bundle2=" + bundle2);
println("result=" + result);
Assert.assertEquals(value, result);
}
void 通过bytes验证Bundle的双向转换(Function<Bundle, byte[]> marshall, Function<byte[], Bundle> unmarshall) {
实验Bundle的转换一致性(bundle1 -> {
byte[] bytes = marshall.apply(bundle1);
System.out.println("bytes=" + deepToString(bytes));
Bundle bundle2 = unmarshall.apply(bytes);
return bundle2;
});
}
@Test
public void 演示Bundle到bytes的序列化和反序列化() {
通过bytes验证Bundle的双向转换(bundle -> {
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}, bytes -> {
Parcel parcel = Parcel.obtain();
try {
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
return Bundle.CREATOR.createFromParcel(parcel);
} finally {
parcel.recycle();
}
});
}
用例运行结果:
bundle1=Bundle[{key=value}]
bytes=[ac,ed,00,05,77,08,00,00,00,06,00,00,00,04,73,72,00,11,6a,61,76,61,2e,6c,61,6e,67,2e,49,6e,74,65,67,65,72,12,e2,a0,a4,f7,81,87,38,02,00,01,49,00,05,76,61,6c,75,65,78,72,00,10,6a,61,76,61,2e,6c,61,6e,67,2e,4e,75,6d,62,65,72,86,ac,95,1d,0b,94,e0,8b,02,00,00,78,70,00,00,00,24,77,04,00,00,00,04,73,71,00,7e,00,00,4c,44,4e,42,77,04,00,00,00,04,73,71,00,7e,00,00,00,00,00,01,77,04,00,00,00,0c,74,00,03,6b,65,79,77,04,00,00,00,04,73,71,00,7e,00,00,00,00,00,00,77,04,00,00,00,10,74,00,05,76,61,6c,75,65]
bundle2=Bundle[{key=value}]
result=value
JSON
本文对JSON的讨论范围仅限于android.jar自带的org.json.JSONObject
类,且不借助与第三方库。
JSONObject
可以与String
相互转换:
// JSONObject -> String
String str = json.toString();
// String -> JSONObject
json = new JSONObject(str);
下面的用例验证了JSONObject
和String
的相互转换效果:
@Test
public void 验证JSONObject与String的双向转换() throws JSONException {
JSONObject x = new JSONObject();
x.put("query", "Pizza");
x.put("price", 3.5);
System.out.println("x=" + x);
System.out.println("0=" + x.toString());
System.out.println("4=" + x.toString(4));
String str = x.toString(4);
JSONObject y = new JSONObject(str);
System.out.println("y=" + y);
x.keys().forEachRemaining(key -> Assert.assertEquals(x.opt(key), y.opt(key)));
y.keys().forEachRemaining(key -> Assert.assertEquals(x.opt(key), y.opt(key)));
}
用例运行结果:
x={"query":"Pizza","price":3.5}
0={"query":"Pizza","price":3.5}
4={
"query": "Pizza",
"price": 3.5
}
y={"query":"Pizza","price":3.5}
Protobuf
本节内容基于以下依赖:
dependencies {
implementation 'com.google.protobuf:protobuf-java:3.21.2'
implementation 'com.google.protobuf:protobuf-java-util:3.21.2'
}
把Protobuf序列化为byte[]
Protobuf不是由Android提供的,Protobuf支持多种语言、多种平台。
Protobuf的message对象可以序列化为byte[]
。假设有如下message定义:
syntax = "proto2";
message Person {
optional string name = 1;
optional string email = 2;
}
Person
对象序列化为byte[]
:
byte[] bytes = person.toByteArray();
从byte[]
反序列化:
person = Person.parseFrom(bytes);
下面的用例验证了Person
对象和byte[]
互相转换的效果:
@Test
public void 验证Protobuf的message与bytes的互相转换效果() throws InvalidProtocolBufferException {
Person from = Person.newBuilder().setName("tom").setEmail("tom@mail.com").build();
println("from=" + from);
byte[] bytes = from.toByteArray();
println("bytes=" + deepToString(bytes));
Person to = Person.parseFrom(bytes);
println("to=" + to);
Assert.assertEquals(from, to);
}
用例运行结果:
from=name: "tom"
email: "tom@mail.com"
bytes=[0a,03,74,6f,6d,12,0c,74,6f,6d,40,6d,61,69,6c,2e,63,6f,6d]
to=name: "tom"
email: "tom@mail.com"
Protobuf与JSON互转
Protobuf的message可以与JSON互转。下面的用例依赖了protobuf-java-util
:
@Test
public void 验证Protobuf的message与JSON的互转效果() throws InvalidProtocolBufferException {
Person from = Person.newBuilder().setName("tom").setEmail("tom@mail.com").build();
System.out.println("from=" + from);
// protobuf -> json
String jsonStr = JsonFormat.printer().print(from);
System.out.println("jsonStr=" + jsonStr);
JSONObject json = null;
try {
json = new JSONObject(jsonStr);
System.out.println("json=" + json);
} catch (JSONException e) {
Assert.fail(e.toString());
}
// json -> protobuf
Builder builder = Person.newBuilder();
JsonFormat.parser().merge(json.toString(), builder);
Person to = builder.build();
System.out.println("to=" + to);
Assert.assertEquals(from, to);
}
用例运行结果:
from=name: "tom"
email: "tom@mail.com"
jsonStr={
"name": "tom",
"email": "tom@mail.com"
}
json={"name":"tom","email":"tom@mail.com"}
to=name: "tom"
email: "tom@mail.com"
更多介绍可跳转:Protobuf官网 和 javadoc。
总结
用一张图来归纳本文的关键内容: