对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。

参考资料

相关推荐
sheji526119 分钟前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法
毕设源码-邱学长20 分钟前
【开题答辩全过程】以 基于Java Web的电子商务网站的用户行为分析与个性化推荐系统为例,包含答辩的问题和答案
java·开发语言
摇滚侠35 分钟前
Java项目教程《尚庭公寓》java项目从开发到部署,技术储备,MybatisPlus、MybatisX
java·开发语言
€8111 小时前
Java入门级教程24——Vert.x的学习
java·开发语言·学习·thymeleaf·数据库操作·vert.x的路由处理机制·datadex实战
Mr_star_galaxy1 小时前
【JAVA】经典图书管理系统的实现
java
昊坤说不出的梦1 小时前
【实战】监控上下文切换及其优化方案
java·后端
冠希陈、1 小时前
PHP 判断是否是移动端,更新鸿蒙系统
android·开发语言·php
今天_也很困2 小时前
LeetCode热题100-560. 和为 K 的子数组
java·算法·leetcode
在繁华处2 小时前
线程进阶: 无人机自动防空平台开发教程V2
java·无人机
A懿轩A2 小时前
【Java 基础编程】Java 变量与八大基本数据类型详解:从声明到类型转换,零基础也能看懂
java·开发语言·python