数据的序列化在 Android 开发中占据着重要的地位,无论是在进程间通信、本地数据存储又或者是网络数据传输都离不开序列化的支持。而针对不同场景选择合适的序列化方案对于应用的性能有着极大的影响。
广义上讲,序列化是将数据结构或者对象转换成可用于存储或者传输的数据格式的过程,在序列化期间,数据结构或者对象将其状态信息写入到临时或者持久性存储区中;反序列化是将序列化过程中生成的数据还原成数据结构或者对象的过程。
一、Serializable
Serializable 接口是 Java 语言的特性,是最简单也是使用最广泛的序列化方案之一,只有实现了Serializable 接口的 Java 对象才可以实现序列化。这边需要注意的一点是 Serializable 接口是一个标识接口,无需实现方法,Java 便会对这个对象进行序列化操作。缺点是使用反射机制,在序列化的过程中会创建很多临时对象,容易触发垃圾机制,序列化的过程比较慢,对于性能要求很严格的场景不建议使用这种方案。
在这里实现了 Serializable 接口的对象才可以序列化,将 Java 对象转换成字节序列,而对应的反序列化则是将字节序列恢复成 Java 对象的过程。
在需要序列化的类中会用到 serialVersionUID 去标识这个序列化对象,即仅当序列化后的数据中的SerialVersionUID 与当前类的 serialVersionUID 相同时才能被正常的反序列化。
import java.io.*;
public class User implements Serializable{
private static final long serialVersionUID = 123456;
public int userId;
public String userName;
public boolean isMale;
public User(int userId, String userName, boolean isMale){
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
public boolean toSerial(User user) throws IOException{
ObjectOutputStream out = null;
boolean status = false;
try {
out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
status = true;
} catch (FileNotFoundException e) {
System.out.println("NO FILE");
} finally {
if(out != null) {
out.close();
}
}
return status;
}
public User toObject(String filename) throws IOException {
ObjectInputStream in = null;
boolean status = false;
User user = null;
try {
in = new ObjectInputStream(new FileInputStream(filename));
user = (User) in.readObject();
} catch(ClassNotFoundException e) {
System.out.println("No file");
} finally {
if(in!=null) {
in.close();
}
}
return user;
}
public static void main(String[] args) throws IOException{
User user = new User(0,"jake",true);
System.out.println(user.toSerial(user));
System.out.println(user.toObject("cache.txt").getClass());
}
}
需要注意的:静态成员变量是属于类而不属于对象的,所以显然它不会参与到对象的序列化过程中。其次用 transient 关键字标记的成员变量不参与到序列化过程中。最后,这种序列化方式是基于磁盘或者网络的。
二、Parcelable
Parcelable 是 Android SDK 提供的,它是基于内存的,由于内存读写速度高于硬盘,因此 Android 中的跨进程对象的传递一般使用 Parcelable。
import android.os.Parcel;
import android.os.Parcelable;
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
public static final Creator CREATOR = new Creator() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
public User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readInt()== 1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale?1:0);
}
}
从上面可以看出,实现一个 Parcelable 接口,需要实现以下几个方法:
- 构造函数:从序列化后的对象中创建原始对象
- describeContents:接口内容的描述,一般默认返回 0 即可
- writeToParcel:序列化的方法,将类的数据写到 parcel 容器中
- 静态的 parcelable.Creator 接口,这个接口包含两个方法:
createFormParcel:反序列化的方法,将 Parcel 还原成 Java 对象
newArray:提供给外部类反序列化这个数组使用。
两种对象序列化方法的对比
Serializable 是 Java 中的序列化接口,其使用起来简单但开销较大(因为 Serializable 在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的 GC),并且在读写数据过程中,它是通过 IO 流的形式将数据写入到硬盘或者传输到网络上。
而 Parcelable 则是以 IBinder 作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用 Parcelable,而 Parcelable 对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用 Serializable。
另外 Serializable 在使用时比较简单,而 Parcelable 在使用时需要手动去实现接口中的方法,为了规避使用 Parcelable 接口时的麻烦,我们下面介绍一个插件,从而自动生成对应的代码。
安装完成后,重启 Android Studio 使其生效,接着编写 Java 实体类 BookItem
public class BookItem {
public String mName;
public long mLastTime;
public String mTitle;
public String mPath;
}
在 Generate 界面中点击 Parceable,该插件自动帮我们将 BookItem 类转换成实现 Parceable 接口的形式,免去开发者手动编写的麻烦,生成代码如下:
public class BookItem implements Parcelable {
public String mName;
public long mLastTime;
public String mTitle;
public String mPath;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.mName);
dest.writeLong(this.mLastTime);
dest.writeString(this.mTitle);
dest.writeString(this.mPath);
}
public BookItem() {
}
protected BookItem(Parcel in) {
this.mName = in.readString();
this.mLastTime = in.readLong();
this.mTitle = in.readString();
this.mPath = in.readString();
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
@Override
public BookItem createFromParcel(Parcel source) {
return new BookItem(source);
}
@Override
public BookItem[] newArray(int size) {
return new BookItem[size];
}
};
}
三、其他的数据序列化方案
接下来的几种方案针对于数据的传输和存储过程中的序列化方案。
1. SQLite
SQLite 是一款轻量级的关系型数据库,它的运行速度极快、占用资源很少—通常只需要几百 kb 的内存即可。主要用于存储复杂的关系型数据,Android 支持原生支持 SQLite 数据库相关操作(SQLiteOpenHelper),不过由于原生 API 接口并不友好,所以产生了不少封装了 SQLite 的 ORM 框架。
2. SharedPreferences
SharedPreferences 是 Android 平台上提供的一个轻量级存储 API,一般用于存储常用的配置信息,其本质是一个键值对存储,支持常用的数据类型如 boolean、float、int、long 以及 String 的存储和读取。
// 获取 Sharedpreferences 对象
SharedPreferences mPreferences = context.getCSharedPreferences(
PREFERENCES_NAME,Context.MODE_PRIVATE);
// 获取 SharedPreferences.Editor 对象
SharedPreferences.Editor editor = mPreferences.edit();
// 写入数据
mEditor.putBoolean(key,b);
// 将写入的数据提交
mEditor.commit();
// 通过 SharedPReferences 对象读取数据
mPreferences.getBoolean(key,defValue);
3. JSON
JSON 全称是 JavaSript Object Notation,是一种轻量级的数据交互格式,由于其相对于 XML,体积更小,在网络上传输时更加介绍浏览,被广泛用于移动端。大部分 APP 与服务端的通信都是使用 JSON 格式进行交互。
4 .Protocol Buffers 及 Nano-Proto-Buffers
Protocol Buffers 是 Google 设计的语言无关、平台无关的一种轻便高效的序列化结构数据存数格式,类似 XML,但更小、更快、更简单、很适合做数据存储或者 RPC 数据交换的格式。它可用于通讯协议,数据存储等领域的与语言无关、平台无关,可扩展的序列化结构数据格式。
在移动端应该使用 Nano-Proto-Buffers 版本。因为普通的 Protocol Buffers 会生成非常冗长的代码,可能会增加 APP 内存占用,导致 APK 体积增长、性能下降,不注意的话,会很快遇到 64K 方法数限制问题。
5. FlatBuffers
Google FlatBuffers 是 Google 为游戏开发或者其他对性能敏感的应用程序创建的开源的,跨平台的,高效的序列化函数库,它提供了对 C++/Java 等语言接口的支持。FlatBuffers 是一个注重性能和资源使用的序列化类库。相比较 Protocol Buffers 而言,它更适合移动设备。
FlatBuffers 的特性主要有:
- 序列化过程不需要打包和拆包:FlatBuffers 将序列化数据存储在缓存中,这些数据既可以存储在文件中,也可以通过网络进行传输,而无需其他任何解析开销,相比之下,Protocol Buffers 和 JSON 等均需要拆包和解析这两个步骤,FlatBuffers 的结构化数据以二进制形式保存,不存在数据解析的过程。
- 内存占用少,性能高:数据访问时唯一的内存需求就是缓冲区,不需要额外的内存分配。
- 强类型系统设计:在编译阶段就能够发现尽可能多的错误,而不是等到运行期才手动检查和修正。
- 支持跨平台:支持 C++/Java 等,不需要其他依赖库。
FlatBuffers 利用自身特殊的编码格式,子啊一定程度上减少内存的使用,优化了数据读取性能。同时,对于数据结构的前向后向兼容提供了良好的可扩展性。