序列化
计算机中最原始的数据就是0和1,有电才能驱动计算机运行,1和0对应电的正负,易经有太极生两仪,两仪生四象,四象生八卦,八卦衍万物,所以0和1可衍生万物,万物皆对象,那这对象怎么保存,最简单就是保存0和1就行,对象转变成0和1就是序列化,0和1转成对象就是反序列化
存储数据和传输数据
简单举的例子:
存储:在java中的面向对象编程中,数据通常是用一个字段表示,如名字name=张三,字段为name,value为张三,这个字段在一个类对象中,这个对象是在程序运行中创建在内存中的,持久保存"张三"就需要把这个对象序列化保存在硬盘上。
传输:传输和存储是一样的,上面说的存储就是内存传硬盘,另一种场景,两台计算机之间传输数据,序列化后的数据是0和1的2进制串,比如两台计算机连着一根金属线,在这跟金属线上通电与断电表示1或0,这样另一台计算机得到2进制串后进行反序列化,就可以得到内容相同的对象了。
序列化故名思意就是按序排列变化,"序"就是某种协议,按这种协议排列变成另一种形态
简单的概括
- 序列化: 主要用于网络传输,数据持久化,一般序列化也称为编码(Encode)
- 反序列化: 主要用于从网络,磁盘上读取字节数组还原成原始对象,一般反序列化也称为解码(Decode)
以上得知序列化是将一个对象的数据保存或传输,怎么样使这个对象能被序列化?
Serializable:可序列化的
Serializable是一个空接口,中文意思使可序列化的,意思使实现了这个接口的javaBean类可以序列化成2进制串,使用ObjectOutputStream 序列化,ObjectInputStream 反序列化。
public class SPerson implements Serializable {
//serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 //serialVersionUID 与发送方发送的 serialVersionUID 不一致,进行反序列时会抛出 InvalidClassException
//
private static final long serialVersionUID = 2088008379212083287L;
private String name;
private int age;
}
public class SerializeableTest {
public static void main(String[] args){
SPerson sPerson1 =new SPerson();
sPerson1.setName("张三");
sPerson1.setAge(18);
System.out.println("serializable="+ sPerson1.getName()+"--"+ sPerson1.getAge());
try {
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(sPerson1);
byte[] bs2= baos.toByteArray();
System.out.println("serializable序列化后="+ Arrays.toString(bs2));
ObjectInputStream ojis=new ObjectInputStream(new ByteArrayInputStream(bs2));
SPerson sPerson2 = (SPerson) ojis.readObject();
System.out.println("serializable反序列化="+ sPerson2.getName()+"--"+ sPerson2.getAge());
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印结果:
serializable=张三--18
serializable序列化后=[-84, -19, 0, 5, 115, 114, 0, 39, 99, 111, 109, 46, 100, 98, 102, 46, 106, 97, 118, 97, 115, 116, 117, 100, 121, 46, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 46, 83, 80, 101, 114, 115, 111, 110, 28, -6, 24, -114, -30, -7, -80, 87, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 18, 116, 0, 6, -27, -68, -96, -28, -72, -119]
serializable反序列化=张三--18
序列化后的文件直接用sublime打开如下
aced 0005 7372 002e 636f 6d2e 7a65 726f
2e73 6572 6961 6c69 7a61 626c 6564 656d
6f2e 7365 7269 616c 697a 6162 6c65 2e53
7475 6465 6e74 e2d9 8cd7 833d f19e 0200
...
- AC ED: STREAM_MAGIC. 声明使用了序列化协议.
- 00 05: STREAM_VERSION. 序列化协议版本.
- 0x73: TC_OBJECT. 声明这是一个新的对象.
- 0x72: TC_CLASSDESC. 声明这里开始一个新Class。
- 00 2e: Class名字的长度.
不容易发现的问题:
SPerson sPerson1 =new SPerson();
sPerson1.setName("张三");
sPerson1.setAge(18);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(sPerson1);
sPerson1.setName("李四");
//oos.writeUnshared(sPerson1);不共享
oos.writeObject(sPerson1);共享
ObjectInputStream ojis=new ObjectInputStream(new ByteArrayInputStream(bs2));
SPerson sPerson2 = (SPerson) ojis.readObject();
SPerson sPerson3 = (SPerson) ojis.readObject();
//以上是想保存一个李四18,设置李四后调用oos.writeObject(sPerson1) 反序列化出来的是两个张三,得用oos.writeUnshared(sPerson1);
Parcelable 可打包的
Parcelable是Android为我们提供的序列化的接口,Parcelable相对于Serializable的使用相对复杂一些,但Parcelable的效率相对Serializable也高很多,Parcelable vs Serializable 号称快10倍的效率
Parcelable是Android SDK提供的,它是基于内存的,由于内存读写速度高于硬盘,因此Android中的跨进程对象的传递一般使用Parcelable
public class PPerson implements Parcelable {
private String name;
private int age;
//反序列化时构建对象,序列化的数据包在Parcel中,Parcel就是包裹的意思
protected PPerson(Parcel in) {
name = in.readString();
age = in.readInt();
}
//构建器,构造一个PPerson对象和PPerson数组对象
public static final Creator<PPerson> CREATOR = new Creator<PPerson>() {
@Override
public PPerson createFromParcel(Parcel in) {
return new PPerson(in);
}
@Override
public PPerson[] newArray(int size) {
return new PPerson[size];
}
};
//描述包含在这个 Parcelable 实例的封送表示中的特殊对象的种类。例如,如果对象将在 {@link writeToParcel(Parcel, int)} 的输出中包含文件描述符,则此方法的返回值必须包含 {@link CONTENTS_FILE_DESCRIPTOR} 位。 @return 一个位掩码,指示由此 Parcelable 对象实例编组的一组特殊对象类型。
@Override
public int describeContents() {
return 0;
}
//将数据打包到Parcel
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
}
}
Android也是基于linux,进程之间内存不共享,因此进程之间的信息传递需要序列化与反序列化,通常在四大组件之间传递对象需要将对象序列化,启动一个Activity交给AMS完成,AMS全名ActivityManagerService,是系统服务,是另一个进程,这里就得序列化了,关于启动四大组件是比较复杂的流程,可以看看下面的文章
startActivity启动过程分析:http://gityuan.com/2016/03/12/start-activity/
Android中Intent/Bundle的通信原理及大小限制
速度快,比如两个Activity之间传递,Activity用于页面展示,速度一定得快,其次数据量也不能过大,大数据传输,肯定会导致速度变慢,能感觉页面明显切换得慢了,数据过大时会报一个错误,
Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的(有些手机是 2 M),而一个进程默认有 16 个 Binder 线程,所以一个线程能占用的缓冲区就更小了( 有人以前做过测试,大约一个线程可以占用 128 KB)。所以当你看到 The Binder transaction failed because it was too large 这类 TransactionTooLargeException 异常时,你应该知道怎么解决了
怎么实现跨进程通信,通过AIDL快速生成代码,下面是生成的部分代码
@Override public void addPerson(com.dbf.studyandtest.myaidl.Person person) throws android.os.RemoteException
{
//先获得传入和传出的打包对象Parcel,看到obtain应该是用了享元模式,与handler传消息的message一样,对象频繁使用,减少new,重复利用已经new好的对象
android.os.Parcel _data = android.os.Parcel.obtain();//传入数据,就是给另一个进程的数据
android.os.Parcel _reply = android.os.Parcel.obtain();//传出数据,就是另一个进程回传的数据
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((person!=null)) {
_data.writeInt(1);
person.writeToParcel(_data, 0);//将数据写入Parcel,可以看到我们实现Parcelable的类,调用的是native方法
}
else {
_data.writeInt(0);
}
boolean _status = mRemote.transact(Stub.TRANSACTION_addPerson, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().addPerson(person);
return;
}
_reply.readException();
if ((0!=_reply.readInt())) {
person.readFromParcel(_reply);//从Parcel读出数据,可以看到我们实现Parcelable的类,也是调用native方法
}
}
finally {
//最后释放
_reply.recycle();
_data.recycle();
}
}
linux中分用户空间和内核空间,先从用户空间其中一个进程cp到内核空间,再从内核空间映射到用户空间的另一个进程,想要达到这个目的要继承Binder实现IInterface,代码形式都是固定的,所以有了AIDL,Android Interface Definition Language,即Android接口定义语言
Parcelable与Serializable的性能比较
Serializable性能分析
Serializable是Java中的序列化接口,其使用起来简单但开销较大(因为Serializable在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过IO流的形式将数据写入到硬盘或者传输到网络上。
Parcelable性能分析
Parcelable则是以IBinder作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用Parcelable,而Parcelable对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用Serializable。
性能比较总结描述
首先Parcelable的性能要强于Serializable的原因我需要简单的阐述一下
- 在内存的使用中,前者在性能方面要强于后者
- 后者在序列化操作的时候会产生大量的临时变量,(原因是使用了反射机制)从而导致GC的频繁调用,因此在性能上会稍微逊色
- Parcelable是以Ibinder作为信息载体的.在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android推荐使用Parcelable,既然是内存方面比价有优势,那么自然就要优先选择.
- 在读写数据的时候,Parcelable是在内存中直接进行读写,而Serializable是通过使用IO流的形式将数据读写入在硬盘上.
但是:虽然Parcelable的性能要强于Serializable,但是仍然有特殊的情况需要使用Serializable,而不去使用Parcelable,因为Parcelable无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化.(原因是在不同的Android版本当中,Parcelable可能会不同,因此数据的持久化方面仍然是使用Serializable)
Protobuf
- 序列化数据非常简洁,紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10。
- 解析速度非常快,比对应的 XML 快约 20-100 倍。
一、配置protobuf
插件github地址:https://github.com/google/protobuf-gradle-plugin
1.在项目根目录的build.gradle中配置:
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'//protobuf插件
}
2.在模块中的build.gradle中配置:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.protobuf'//protobuf插件
android {
...
sourceSets {
main {
java {
srcDir 'src/main/java'
}
proto {
srcDir 'src/main/proto'//添加protobuf目录,在里面编写需要序列化的文件
}
}
}
}
protobuf {
//配置protoc编译器
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'//这里的版本跟斜面依赖的版本一致
}
//这里配置生成目录,编译后会在build的目录下生成对应的java文件
generateProtoTasks {
all().each { task ->
task.builtins {
remove java
}
task.builtins {
java {}
}
}
}
}
dependencies {
implementation 'com.google.protobuf:protobuf-java:3.8.0'
}
3.Settings Plugins 搜索安装 Protobuf Support 插件,装好后可以识别出.proto文件
二、编写protobuf
.proto文件编写 github地址:https://github.com/protocolbuffers/protobuf
proto3本版本java:https://developers.google.com/protocol-buffers/docs/proto3
例子:
syntax = "proto3";//版本 之前有proto2
//package tutorial;
/**java_multiple_files = true
选项可以生成单独的.java每个生成的类的文件
(而不是.java为包装类生成单个文件的遗留行为,使用包装类作为外部类,并将所有其他类嵌套在包装类中)。
*/
option java_multiple_files = true;
/** java_package指定生成的类应该使用的 Java 包名称。
如果您没有明确指定,它只是匹配package声明给出的包名,
但这些名称通常不是合适的 Java 包名(因为它们通常不以域名开头)。
*/
option java_package = "com.dbf.studyandtest.proto";
/**java_outer_classname选项定义将表示此文件的包装类的类名。
如果你不java_outer_classname明确给出a ,它会通过将文件名转换为大写驼峰来生成。
例如,默认情况下,“my_proto.proto”将使用“MyProto”作为包装类名称。
*/
option java_outer_classname = "_Person";
message Person{
/**每个元素上的“= 1”、“= 2”标记标识字段在二进制编码中使用的唯一“标签”。
标记数字 1-15 需要比更高数字少一个字节来编码,因此作为一种优化,您可以决定将这些标记用于常用或重复的元素,
而将标记 16 和更高的标记用于不太常用的可选元素。重复字段中的每个元素都需要重新编码标签编号,因此重复字段特别适合这种优化。
*/
//指定字段的类型 定义字段的编号,在Protocol Buffers中,字段的编号非常重要,字段名仅仅是作为参考和生成代码用。需要注意的是字段的编号区间范围,其中19000 ~ 19999被Protocol Buffers作为保留字段
string name = 1;
int32 age = 2;
// message Home{
// string address = 1;
// }
// Home home = 3;
}
//message Family {
// Person person = 1;
//}
Make project 一下自动生成代码
/**
* Protobuf type {@code Person}
*/
public final class Person extends com.google.protobuf.GeneratedMessageV3 implements PersonOrBuilder {
//省略600多行代码
}
与Serializable对比
public static void main(String[] args) throws Exception{
Person person = Person.newBuilder().setName("张三").setAge(18).build();
System.out.println("protobuf="+person.getName()+"--"+person.getAge());
//序列化
byte[] bs=person.toByteArray();
System.out.println("protobuf序列化后="+Arrays.toString(bs));
Person person2=Person.parseFrom(bs);
System.out.println("protobuf反序列化="+person2.getName()+"--"+person2.getAge());
System.out.println("\n----------------分割线-------------\n");
SPerson sPerson1 =new SPerson();
sPerson1.setName("张三");
sPerson1.setAge(18);
System.out.println("serializable="+ sPerson1.getName()+"--"+ sPerson1.getAge());
try {
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(sPerson1);
byte[] bs2= baos.toByteArray();
System.out.println("serializable序列化后="+Arrays.toString(bs2));
ObjectInputStream ojis=new ObjectInputStream(new ByteArrayInputStream(bs2));
SPerson sPerson2 = (SPerson) ojis.readObject();
System.out.println("serializable反序列化="+ sPerson2.getName()+"--"+ sPerson2.getAge());
} catch (Exception e) {
e.printStackTrace();
}
}
打印结果:
protobuf=张三--18
protobuf序列化后=[10, 6, -27, -68, -96, -28, -72, -119, 16, 18]
protobuf反序列化=张三--18
----------------分割线-------------
serializable=张三--18
serializable序列化后=[-84, -19, 0, 5, 115, 114, 0, 39, 99, 111, 109, 46, 100, 98, 102, 46, 106, 97, 118, 97, 115, 116, 117, 100, 121, 46, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 46, 83, 80, 101, 114, 115, 111, 110, 28, -6, 24, -114, -30, -7, -80, 87, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 0, 0, 0, 18, 116, 0, 6, -27, -68, -96, -28, -72, -119]
serializable反序列化=张三--18
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | 使用可变长度编码。 编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint32。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | 使用可变长度编码。 编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint64。 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
uint32 | 使用可变长度编码。 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | 使用可变长度编码。 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sint32 | 使用可变长度编码。 有符号整数值。 这些比常规 int32 更有效地编码负数。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | 使用可变长度编码。 有符号整数值。 这些比常规 int64 更有效地编码负数。 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
fixed32 | 总是四个字节。 如果值通常大于 228,则比 uint32 更有效。 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 总是八个字节。 如果值通常大于 256,则比 uint64 更有效。 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | 总是四个字节。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 总是八个字节。 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。 | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | 可以包含不超过 232 的任意字节序列。 | string | ByteString | str (Python 2) bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string |