Parcelable:Android的高效序列化方案
Parcelable
是Android专为跨进程通信优化的序列化接口,主要用于组件间和跨进程的数据传输,相比Java中的Serializable
,优势在于性能更高,直接读写内存,避免了反射和临时对象的创建。
import android.os.Parcel;
import android.os.Parcelable;
public class JParcelable implements Parcelable {
private int id;
private String name;
public JParcelable(int id, String name) {
this.id = id;
this.name = name;
}
private JParcelable(Parcel in) {
id = in.readInt();
name = in.readString();
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(id);
out.writeString(name);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<JParcelable> CREATOR
= new Parcelable.Creator<JParcelable>() {
@Override
public JParcelable createFromParcel(Parcel in) {
return new JParcelable(in);
}
@Override
public JParcelable[] newArray(int size) {
return new JParcelable[size];
}
};
}
在kotlin中,可以使用@Parcelize
注解减少编写大量样板代码,如下:
//在build.gradle文件中添加插件
plugins {
id 'kotlin-parcelize'
}
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class KParcelable(val id: Int, val name: String) : Parcelable
知识点1:Parcelable接口中describeContents()
方法的作用
按照Google官方说法,describeContents()
方法主要用于标识当前对象是否包含文件描述符,以便系统在序列化和反序列化时进行相应的处理,确保文件描述符可以在不同的进程之间安全、高效地传递。
返回值是一个整数,为0
或CONTENTS_FILE_DESCRIPTOR
。
0
:默认值,表示对象中没有需要特殊处理的内容。CONTENTS_FILE_DESCRIPTOR
:表示对象包含文件描述符,此时系统会额外处理文件描述符的生命周期(如复制或关闭)。
//android/os/Parcelable.java
/**
* Descriptor bit used with {@link #describeContents()}: indicates that
* the Parcelable object's flattened representation includes a file descriptor.
*
* @see #describeContents()
*/
public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001;
ParcelFileDescriptor是Android中用于进程间通信的一个类,它封装了一个文件描述符,可以在进程之间直接传递文件描述符,避免了在进程间复制文件数据,同时该类实现了Parcelable接口,对应describeContents()
方法的返回值为CONTENTS_FILE_DESCRIPTOR
//android/os/ParcelFileDescriptor.java
public class ParcelFileDescriptor implements Parcelable, Closeable {
private static final String TAG = "ParcelFileDescriptor";
private final FileDescriptor mFd;
/**
* Optional socket used to communicate close events, status at close, and
* detect remote process crashes.
*/
private FileDescriptor mCommFd;
/**
* Wrapped {@link ParcelFileDescriptor}, if any. Used to avoid
* double-closing {@link #mFd}.
* mClosed is always true if mWrapped is non-null.
*/
private final ParcelFileDescriptor mWrapped;
@Override
public int describeContents() {
if (mWrapped != null) {
return mWrapped.describeContents();
} else {
return Parcelable.CONTENTS_FILE_DESCRIPTOR;
}
}
}
那么describeContents()
方法在哪里有被调用呢?目前只发现在Parcel类的静态方法hasFileDescriptors
里有用到,如下:
//android/os/Parcel.java
/**
* Check if the object has file descriptors.
*
* <p>Objects supported are {@link Parcel} and objects that can be passed to {@link
* #writeValue(Object)}}
*
* <p>For most cases, it will use the self-reported {@link Parcelable#describeContents()} method
* for that.
*
* @throws IllegalArgumentException if you provide any object not supported by above methods
* (including if the unsupported object is inside a nested container).
*
* @hide
*/
public static boolean hasFileDescriptors(Object value) {
if (value instanceof Parcel) {
Parcel parcel = (Parcel) value;
if (parcel.hasFileDescriptors()) {
return true;
}
} else if (value instanceof LazyValue) {
LazyValue lazy = (LazyValue) value;
if (lazy.hasFileDescriptors()) {
return true;
}
} else if (value instanceof Parcelable) {
Parcelable parcelable = (Parcelable) value;
if ((parcelable.describeContents() & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) {
return true;
}
}
......
}
知识点2:Java枚举单例为什么可以防止反序列化破坏和攻击
一个典型的枚举单例实现如下:
package io.github.kongpf8848;
// 枚举单例,线程安全,防止反射、序列化破坏
public enum Singleton {
INSTANCE;
public void doSomething() {
}
}
枚举类型在序列化和反序列化时具有特殊行为:
- 序列化时:仅保存枚举常量的名称(如
INSTANCE
)。 - 反序列化时:通过
Enum.valueOf(Class<T> enumClass, String name)
方法,根据名称查找已有的枚举常量实例,而非创建新对象。
底层实现
- 序列化:枚举常量的序列化格式仅包含其名称。
我们通过一个例子验证一下,将Singleton实例序列化写入enum.txt,对应的代码如下:
//序列化枚举单例
FileOutputStream fileOutputStream = new FileOutputStream("enum.txt");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(Singleton.INSTANCE);
outputStream.flush();
outputStream.close();
用16进制编辑器打开生成的enum.txt文件,内容如下:
序列化文件分为文件头
、类描述
、字段描述
3部分
- 文件头
内容 | 说明 |
---|---|
ACED | 序列化魔法数 |
0005 | 版本号 |
- 类描述
内容 | 说明 |
---|---|
7E | 枚举对象标识 |
72 | 类描述开始标识 |
001E | 类名长度,30 |
696F2E67 69746875 622E6B6F 6E677066 38383438 2E53696E 676C6574 6F6E | 类名,io.github.kongpf8848.Singleton |
00000000 00000000 | serialVersionUID |
12 | flag标识 |
00 00 | 字段的总数,0 |
78 | 类描述结束标识 |
72 | 类描述开始标识 |
000E | 类名长度,14 |
6A617661 2E6C616E 672E456E 756D | 类名,java.lang.Enum |
00000000 00000000 | serialVersionUID |
12 | flag标识 |
00 00 | 字段的总数,0 |
78 | 类描述结束标识 |
70 | 空引用, TC_NULL |
- 字段描述
内容 | 说明 |
---|---|
74 | 字段类型为String |
0004 | 字段长度,8 |
494E5354 414E4345 | 字段名称,INSTANCE |
- 反序列化:
ObjectInputStream
在读取枚举时,调用Enum.valueOf()
方法,通过名称匹配已有实例。
我们通过一个例子验证一下,将序列化文件enum.txt转化为枚举常量,对应的代码如下:
//反序列化枚举单例
FileInputStream fileInputStream = new FileInputStream("enum.txt");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
Singleton other = (Singleton) inputStream.readObject();
System.out.println(other == Singleton.INSTANCE); //输出结果为true
ObjectInputStream的readObject()代码对应如下:
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}
readObject()方法调用了readObject(Object)方法,代码如下:
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException
{
......
try {
Object obj = readObject0(type, false);
......
return obj;
} finally {
......
}
}
接着看readObject0方法,代码如下:
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
try {
switch (tc) {
......
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
......
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
......
}
}
如果对象标识为枚举,调用readEnum方法,代码如下:
/**
* Reads in and returns enum constant, or null if enum type is
* unresolvable. Sets passHandle to enum constant's assigned handle.
*/
private Enum<?> readEnum(boolean unshared) throws IOException {
......
ObjectStreamClass desc = readClassDesc(false);
......
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
......
return result;
}
最终调用了Enum.valueOf((Class)cl, name)方法,即执行以下语句:
Enum.valueOf(Singleton.class, "INSTANCE");
返回已经存在的枚举常量Singleton.INSTANCE。