对Parcelable/Serializable的一点理解

Parcelable:Android的高效序列化方案

Parcelable是Android专为跨进程通信优化的序列化接口,主要用于组件间和跨进程的数据传输,相比Java中的Serializable,优势在于性能更高,直接读写内存,避免了反射和临时对象的创建。

java 复制代码
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 注解减少编写大量样板代码,如下:

groovy 复制代码
//在build.gradle文件中添加插件

plugins {
    id 'kotlin-parcelize'
}
kotlin 复制代码
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:表示对象包含文件描述符,此时系统会额外处理文件描述符的生命周期(如复制或关闭)。
java 复制代码
//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

java 复制代码
//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里有用到,如下:

java 复制代码
//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枚举单例为什么可以防止反序列化破坏和攻击

一个典型的枚举单例实现如下:

java 复制代码
package io.github.kongpf8848;
// 枚举单例,线程安全,防止反射、序列化破坏
public enum Singleton {
    INSTANCE;

    public void doSomething() {

    }
}

枚举类型在序列化和反序列化时具有特殊行为:

  • 序列化时 :仅保存枚举常量的名称(如INSTANCE)。
  • 反序列化时 :通过Enum.valueOf(Class<T> enumClass, String name)方法,根据名称查找已有的枚举常量实例,而非创建新对象。

底层实现

  • 序列化:枚举常量的序列化格式仅包含其名称。

我们通过一个例子验证一下,将Singleton实例序列化写入enum.txt,对应的代码如下:

java 复制代码
//序列化枚举单例
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 父类描述,70对应为NULL
  • 字段描述
内容 说明
74 字段类型为String
0004 字段长度,8
494E5354 414E4345 字段名称,INSTANCE
  • 反序列化ObjectInputStream在读取枚举时,调用Enum.valueOf()方法,通过名称匹配已有实例。

我们通过一个例子验证一下,将序列化文件enum.txt转化为枚举常量,对应的代码如下:

java 复制代码
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()代码对应如下:

java 复制代码
public final Object readObject()
    throws IOException, ClassNotFoundException {
    return readObject(Object.class);
}

readObject()方法调用了readObject(Object)方法,代码如下:

java 复制代码
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException
{
    ......
    try {
        Object obj = readObject0(type, false);
        ......
        return obj;
    } finally {
       ......
    }
}

接着看readObject0方法,代码如下:

java 复制代码
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方法,代码如下:

java 复制代码
/**
 * 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)方法,即执行以下语句:

java 复制代码
Enum.valueOf(Singleton.class, "INSTANCE");

返回已经存在的枚举常量Singleton.INSTANCE。

参考资料

相关推荐
ybq1951334543119 分钟前
Redis-主从复制-分布式系统
java·数据库·redis
weixin_472339461 小时前
高效处理大体积Excel文件的Java技术方案解析
java·开发语言·excel
小毛驴8501 小时前
Linux 后台启动java jar 程序 nohup java -jar
java·linux·jar
DKPT2 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
好奇的菜鸟3 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
哲科软件4 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
DuelCode4 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社24 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
幽络源小助理4 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码5 小时前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot