对Parcelable/Serializable的一点理解

121 阅读5分钟

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()方法主要用于标识当前对象是否包含文件描述符,以便系统在序列化和反序列化时进行相应的处理,确保文件描述符可以在不同的进程之间安全、高效地传递。

返回值是一个整数,为0CONTENTS_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文件,内容如下: enum.png

序列化文件分为文件头类描述字段描述3部分

  • 文件头
内容说明
ACED序列化魔法数
0005版本号
  • 类描述
内容说明
7E枚举对象标识
72类描述开始标识
001E类名长度,30
696F2E67 69746875 622E6B6F 6E677066 38383438 2E53696E 676C6574 6F6E类名,io.github.kongpf8848.Singleton
00000000 00000000serialVersionUID
12flag标识
00 00字段的总数,0
78类描述结束标识
72类描述开始标识
000E类名长度,14
6A617661 2E6C616E 672E456E 756D类名,java.lang.Enum
00000000 00000000serialVersionUID
12flag标识
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。

参考资料