Android 对象序列化之你不知道的 Serializable

闪存
Android 存储优化系列专题
  • SharedPreferences 系列

Android 之不要滥用 SharedPreferences
Android 之不要滥用 SharedPreferences(2)— 数据丢失

  • ContentProvider 系列(待更)

《Android 存储选项之 ContentProvider 启动存在的暗坑》
《Android 存储选项之 ContentProvider 深入分析》

  • 对象序列化系列

Android 对象序列化之你不知道的 Serializable
Android 对象序列化之 Parcelable 取代 Serializable ?
Android 对象序列化之追求性能完美的 Serial

  • 数据序列化系列(待更)

《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》

  • SQLite 存储系列

Android 存储选项之 SQLiteDatabase 创建过程源码分析
Android 存储选项之 SQLiteDatabase 源码分析
数据库连接池 SQLiteConnectionPool 源码分析
SQLiteDatabase 启用事务源码分析
SQLite 数据库 WAL 模式工作原理简介
SQLite 数据库锁机制与事务简介
SQLite 数据库优化那些事儿


前言

对象序列化系列,主要内容包括:Java 原生提供的 Serializable ,更加适合 Android 平台轻量且高效的 Parcelable,以及追求性能完美的 Serial。该系列内容主要结合源码的角度分析它们各自的优缺点以及合适的使用场景。

今天先来聊聊 Java 原生提供的 Serializable 对象序列化机制,本文不是与大家一起探讨关于 Serializable 的具体使用,相信关于 Serializable 大家肯定也不会感到陌生,Serializable 是 Java 原生的序列化机制,在 Android 中也有被广泛使用。我们可以通过 Serializable 将对象持久化存储,也可以通过 Bundle 传递 Serializable 的序列化数据。

Serializable 简单实现背后包含着复杂的计算逻辑,本文也将结合源码角度深入分析 Serializable 背后实现原理。

序列化/反序列化
  • 什么是对象序列化

大家是否有思考过,什么是序列化?应用程序中的对象存储在内存中,如果此时我们想把对象存储下来或者在网络上传输,这个时候就需要用到对象的序列化和反序列化。

对象序列化就是把一个 Object 对象的所有信息表示成一个字节序列,这包括 Class 信息、继承关系、访问权限、变量类型以及数值信息等。

数据存储不一定就是将数据存放到磁盘中,比如放到内存中、通过网络传输也可以算是存储的一种形式。或者我们也可以把这个过程叫做对象或者数据的序列化。

Serializable 简单使用

“好像我们只需要简单实现 Serializable 接口,它就可以完成序列化/反序列化工作了”, 那它是如何工作的呢?实际上 Serializalbe 这种简单机制是通过牺牲掉执行性能为代价换来的,也就是在时间开销和使用成本的权衡上,Serializable 机制选择是使用成本优先。

public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLike() {
        return like;
    }

    public void setLike(String like) {
        this.like = like;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", like='" + like + '\'' +
                '}';
    }
}

使用 ObjectOutputStream 将 Person 对象序列化到磁盘:

public void testWritePersonObject() {
    Person person;
    ObjectOutputStream out = null;
    try {
        out = new ObjectOutputStream(openFileOutput("person", Context.MODE_PRIVATE));
        person = new Person();
        out.writeObject(person);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        IOUtil.close(out);
    }
}

整个使用过程是不是非常简单?虽然 Serializable 使用真的非常简单,但是也有一些需要注意的事项。另外它还有一些进阶的使用技巧。文中也会详细介绍到,并且最后也会给大家总结出该部分内容。

Serializable 背后实现原理
  • ObjectOutputStream 和 ObjectInputStream

不知道大家是否跟我一样,在最初使用 Serializable 时是否会有这样的疑问:“我们只需要实现这个接口,好像什么都不用管了,那它是如何实现序列化的呢?”

实际上 Serializable 的实现原理分别在 ObjectOutputStream 和 ObjectInputStream 中(本文主要给大家介绍序列化过程),整个序列化过程使用了大量反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化对应引用的其他对象。

先来看下 ObjectObjectStream 的构造方法:

    public ObjectOutputStream(OutputStream out) throws IOException {
    //检查继承权限
    verifySubclass();
    //构造一个BlockDataOutputStream用于向out写入序列化数据
    bout = new BlockDataOutputStream(out);
    //构造一个大小为10,负载因子为3的HandleTable和ReplaceTable
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    //有参构造方法默认为false,
    enableOverride = false;
    writeStreamHeader();
    //将缓存模式打开,写入数据时先写入缓冲区
    bout.setBlockDataMode(true);
    if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
    }
}

我们从上面例子中 ObjectOutputStream writeObject 方法开始说起:

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        //这里判断是否需要重写writeObject()
        //如果使用无参构造方法创建,该变量默认为true,此时必须重写writeObject()
        //此时实际需要重写的方法为:writeObjectOverride,否则抛出异常
        writeObjectOverride(obj);
        return;
    }
    try {
        //执行writeObject0()
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            try {
                writeFatalException(ex);
            } catch (IOException ex2) {}
        }
        throw ex;
    }
}

在 writeObject 方法中首先判断当前是否重写了 writeObject 方法,如果我们使用无参构造方法创建 ObjectOutStream 实例,此时必须重写该方法(实际重写的是 writeObjectOverride 方法),否则会抛出异常。

接着跟踪 writeObject0 方法:

private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        int h;
        //处理以前写过和不可替换的对象
        //如果obj为null,此时一定返回null
        if ((obj = subs.lookup(obj)) == null) {
            writeNull();
            return;
        } else if (!unshared && (h = handles.lookup(obj)) != -1) {
            writeHandle(h);
            return;
        }

        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;

        Class repCl;
        //创建ObjectStreamClass实例
        //ObjectStreamClass表示序列化对象的class详细信息
        desc = ObjectStreamClass.lookup(cl, true);

        if (desc.hasWriteReplaceMethod()
                && (obj = desc.invokeWriteReplace(obj)) != null
                && (repCl = obj.getClass()) != cl) {
            //如果实现了writeReplace方法替换掉原写入对象 与
            //该方法返回对象不为null 与
            //返回对象类型不是当前序列化类型
            cl = repCl;
            //生成替换对象类型的ObjectStreamClass
            desc = ObjectStreamClass.lookup(cl, true);
        }

        if (enableReplace) {
            //如果通过enableReplaceObject()方法设置enablReplace
            //此时回调replaceObject,需要子类重写,否则返回与上述obj一致。
            //这里是继承ObjectOutputStream之后重写了replaceObject方法
            Object rep = replaceObject(obj);
            if (rep != obj && rep != null) {
                cl = rep.getClass();
                desc = ObjectStreamClass.lookup(cl, true);
            }
            //重新赋值要序列化的对象
            obj = rep;
        }

        if (obj != orig) {
            //如果替换了原序列化对象
            subs.assign(orig, obj);
            if (obj == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            }
        }

        // remaining cases
        // BEGIN Android-changed
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        // END Android-changed
        } else if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            //我们只看实现 Serializable 序列化过程
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

该方法中我们重点关心 desc = ObjectStreamClass.lookup 和
writeOrdinaryObject 两个方法。

首先看 ObjectStreamClass.lookup 方法:它返回 ObjectStreamClass 实例,ObjectStreamClass 保存了要序列化对象的 Class 相关信息:Class 名称、serialVersionUID、实现了 Serializable 还是 Externalizable 接口,是否自行实现了 writeObject 和 readObject 方法等内容。每个序列化对象类型对应一个 ObjectStreamClass 实例。

static ObjectStreamClass lookup(Class<?> cl, boolean all) {
    if (!(all || Serializable.class.isAssignableFrom(cl))) {
        //all在ObjectOutStream中默认传递true
        //判断对象类型是否实现了Serializable接口
        //如果all=false 与 未实现Serializable接口则直接return。
        return null;
    }
    //清除失去Class引用的对应ObjectStreamClass实例缓存
    //不过这在Android好像并不能生效,因为在Android JVM 中类是不可以被卸载的
    processQueue(Caches.localDescsQueue, Caches.localDescs);
    //将写入对象类交给弱引用持有,WeakClassKey继承自WeakReference
    WeakClassKey key = new WeakClassKey(cl, Caches.localDescsQueue);
    //根据类对象获取是否存在缓存的EntryFutrue或ObjectStreamClass
    //这里可能大家会有疑问,每次new WeakClassKey() 作为key?实际上它内部使用
    //Class作为hash值的计算
    Reference<?> ref = Caches.localDescs.get(key);
    Object entry = null;
    if (ref != null) {
        //如果引用不为null,获取引用的实际类型
        //在Android中只要是之前缓存过,则该ref永远不为null(Class 不可以被卸载)
        //获取该ref引用的对象
        entry = ref.get();
    }
    EntryFuture future = null;
    //如果引用的对象已经被回收
    if (entry == null) {
        //创建EntryFuture,并使用软引用关联。
        EntryFuture newEntry = new EntryFuture();
        Reference<?> newRef = new SoftReference<>(newEntry);
        do {
            if (ref != null) {
                //此时实际的引用对象已经被回收了,故ref引用就没有用了
                //从缓存中删除该键值对
                Caches.localDescs.remove(key, ref);
            }
            //localDescs是ConcurrentHashMap,
            //putIfAbsent()方法,不存在该key就添加进去,返回null
            //如果存在就返回之前的Reference。
            ref = Caches.localDescs.putIfAbsent(key, newRef);
            if (ref != null) {
                entry = ref.get();
            }
            //这里是要保证该key(Class)对应的Reference中持有的对象没有被回收
        } while (ref != null && entry == null);

        if (entry == null) {
            //如果entry为null,此时future就是新创建的EntryFuture
            future = newEntry;
        }
    }
    if (entry instanceof ObjectStreamClass) {
        //如果拿到的是ObjectStreamClass,直接返回
        return (ObjectStreamClass) entry;
    }
    if (entry instanceof EntryFuture) {
        //如果拿到的是EntryFuture
        future = (EntryFuture) entry;
        if (future.getOwner() == Thread.currentThread()) {
            //如果创建这个EntryFuture线程就是当前线程
            //意思就是当前新创建的这个EntryFuture
            //这里实际判断该future是否是刚创建的newEntry
            entry = null;
        } else {
            //否则就获取之前的set的
            entry = future.get();
        }
    }
    if (entry == null) {
        try {
            //如果是新创建的future,此时就创建新的ObjectStreamClass
            entry = new ObjectStreamClass(cl);
        } catch (Throwable th) {
            entry = th;
        }
        if (future.set(entry)) {
            //EntryFuture中没有关联过entry
            //关联,并缓存为当前key对应引用关系
            Caches.localDescs.put(key, new SoftReference<Object>(entry));
        } else {
            // nested lookup call already set future
            entry = future.get();
        }
    }

    //entr必须为是ObjectStreamClass,否则抛异常
    if (entry instanceof ObjectStreamClass) {
        return (ObjectStreamClass) entry;
    } else if (entry instanceof RuntimeException) {
        throw (RuntimeException) entry;
    } else if (entry instanceof Error) {
        throw (Error) entry;
    } else {
        throw new InternalError("unexpected entry: " + entry);
    }
}

lookup() 方法的第一个 if 语句就判断了要序列化的类是否实现了 Serializable 接口,否则返回 null,此时回到 ObjectOutputStream 的 writeObject0() 方法将会出现异常,这里也证明要序列化对象必须实现 Serializable 接口。

lookup 整个方法看起来有些繁琐,实际是从缓冲中获取该序列化类型对应的 ObjectStreamClass 对象,ObjectStreamClass 的引入一方面是为了代码解耦,另一个重要方面是提高获取类信息的速度(ObjectStreamClass 保存了当前序列化类的相关信息),系统通过 SoftReference 持有 Class 和 ObjectStreamClass 的映射关系。如果反复对一个类的实例进行序列化操作,能够有效减少耗时,提高缓存命中率。

源码中有几个需要说明的地方:

  • WeakClassKey 继承自 WeakReference,这里持有要序列对象的 Class,不过正如在注释中所述:这在 Android 中好像并不能生效,因为在 Android 平台 Java 虚拟机中,类对象是不可以被回收的(这也是出于性能的考虑)。

  • localDescs 是一个 ConcurrentHashMap 对象,源码中可以看到每次 new WeakClassKey() 对象作为 key 获取是否已经缓存过对应的 ObjectStreamClass 实例?实际上 WeakClassKey 重写了 hashCode() 方法,故这里采用的是 Class 对象来计算的 Hash 值。

  • 源码中的 do {} while() 作用是保证该 Class 对象能够正常获取到对应的 ObjectStreamClass 对象。

  • EntryFuture 是 ObjectStreamClass 的内部类,引入它的作用我个人认为是多线程调用 lookup() 方法而设立的。主要体现在判断:if(future.getOwner() == Thread.currentThread()) 如果成立说明此时 EntryFuture 就是刚刚创建的。否则是从缓存中获取到的(但不代表就是其它线程创建的)。

跟踪到这里有必要进一步分析下 ObjectStreamClass 这个类,看下它的构造方法:

private ObjectStreamClass(final Class<?> cl) {
    this.cl = cl;
    //序列化对象的类名称
    name = cl.getName();
    //判断是否是代理类
    isProxy = Proxy.isProxyClass(cl);
    //是否是Enum
    isEnum = Enum.class.isAssignableFrom(cl);
    //是否实现了Serialable接口
    serializable = Serializable.class.isAssignableFrom(cl);
    //是否实现了Externalizable接口
    externalizable = Externalizable.class.isAssignableFrom(cl);
    //获取其父类
    Class<?> superCl = cl.getSuperclass();
    //与之前分析的lookup()方法一致,这里会递归调用每个父类,
    //因为lookup()又会创建对应类的ObjectStramClass
    //superDesc表示其父类的ObjectStreamClass
    superDesc = (superCl != null) ? lookup(superCl, false) : null;
    //表示当前的ObjectStreamClass
    localDesc = this;

    if (serializable) {
        //如果实现了Serializable接口
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                if (isEnum) {
                    suid = Long.valueOf(0);
                    fields = NO_FIELDS;
                    //枚举类型,此时没有 Field
                    return null;
                }
                if (cl.isArray()) {
                    //数组类型
                    fields = NO_FIELDS;
                    return null;
                }
                //生成serialVersionUID
                //会去反射我们有没有声明serialVersionUID
                suid = getDeclaredSUID(cl);
                try {
                    //反射获取该类所有需要被序列化的Field
                    //首先反射是否定义了serialPersistentFields,如果存在则根据其内部声明为准。
                    //否则直接反射获取该类所有的字段(过滤static 和 transient)
                    fields = getSerialFields(cl);
                    computeFieldOffsets();
                } catch (InvalidClassException e) {
                    serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                    fields = NO_FIELDS;
                }

                if (externalizable) {
                    //如果实现了Enternalizable接口,获取其构造方法
                    cons = getExternalizableConstructor(cl);
                } else {
                    //获取构造方法
                    cons = getSerializableConstructor(cl);
                    //反射获取是否实现了writeObject方法
                    writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            //参数类是 ObjectOutputStream
                            new Class<?>[]{ObjectOutputStream.class},
                            Void.TYPE);
                    //反射获取是否实现了readObject方法
                    readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[]{ObjectInputStream.class},
                            Void.TYPE);
                    //反射获取是否实现了readObjectNoData方法
                    readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                    hasWriteObjectData = (writeObjectMethod != null);
                }
                //反射获取是否实现了writeReplace
                writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                //反射获取是否实现了readResolve
                readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                return null;
            }
        });
    } else {
        suid = Long.valueOf(0);
        fields = NO_FIELDS;
    }

    // ... 省略
}

ObjectStreamClass 类的构造方法做的事情还是蛮多的:获取序列化类的相关信息、一系列类型检查、创建其父类的 ObjecStreamClass 实例、通过反射获取当前类的所有字段信息、反射获取是否自行实现了 writeObject()、readObject() 、writeReplace()、readResolve() 等方法。

下面我们就几个关键方法做进一步说明:

  • suid = getDeclaredSUID(cl) 获取 serialVersionUID

系统通过反射的方式判断我们是否有声明 serialVersionUID:

private static Long getDeclaredSUID(Class<?> cl) {
    try {
        //通过反射获取是否定义了serialVersionUID
        Field f = cl.getDeclaredField("serialVersionUID");
        int mask = Modifier.STATIC | Modifier.FINAL;
        if ((f.getModifiers() & mask) == mask) {
            //必须static final声明
            f.setAccessible(true);
            //必须用static声明,否则获取不到
            return Long.valueOf(f.getLong(null));
        }
    } catch (Exception ex) {
    }
    return null;
}

从这里我们可以看出自定义 serialVersionUID 必须声明为 static final。

默认 serialVersionUID 计算规则在 ObjectStreamClass computeDefaultSUID 方法。序列化使用一个 hash,该 hash 是根据给定源文件中几乎所有东西:方法名称、字段名称、字段类型、访问修改方法等-计算出来的,序列化将该 hash 值与序列化流中的 hash 值比较。

为了使 Java 运行时相信两种类型实际上是一样的,第二版和随后版本的 Person 必须与第一版有相同的序列化版本 hash(存储为 private static final serialVersionUID 字段)。因此,我们需要 serialVersionUID 字段,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver 命令计算出的。

一旦有了 Person 的 serialVersionUID,不仅可以从原始对象 Person 的序列化数据创建 PersonV2 对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2 的数据通过反序列化得到 Person。

整个默认 serialVersionUID 计算流程非常繁琐和复杂,通常我建议显示声明会更加稳妥,因为隐士声明假如类发生一点点变化,进行反序列化都会由于 serialVersionUID 改变而导致 InvalidClassException 异常。

  • fields = getSerialFields(cl) 反射获取当前类的所有字段

首先系统会判断我们是否自行实现了字段序列化 serialPersistentFields 属性,否则走默认序列化流程,既忽律 static、transient 字段。

private static ObjectStreamField[] getSerialFields(Class<?> cl)
        throws InvalidClassException {
    ObjectStreamField[] fields;
    //必须实现了Serialiazble,并且不是代理类,不是Externalizable,不是接口类型
    if (Serializable.class.isAssignableFrom(cl)
            && !Externalizable.class.isAssignableFrom(cl)
            && !Proxy.isProxyClass(cl)
            && !cl.isInterface()) {

        //是否声明serialPersistentFields的自定义字段序列化规则
        if ((fields = getDeclaredSerialFields(cl)) == null) {
            //默认序列化字段规则
            fields = getDefaultSerialFields(cl);
        }
        Arrays.sort(fields);
    } else {
        fields = NO_FIELDS;
    }
    return fields;
}

我们先来看下默认序列化流程的字段获取规则:

 private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    //反射获取当前类的所有字段
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    //忽略 static 或 transient 字段
    int mask = Modifier.STATIC | Modifier.TRANSIENT;

    for (int i = 0; i < clFields.length; i++) {
        if ((clFields[i].getModifiers() & mask) == 0) {
            //非static,非transient字段
            //将其封装在ObjectStreamField中
            //ObjectStreamField是对Field封装
            list.add(new ObjectStreamField(clFields[i], false, true));
        }
    }
    int size = list.size();
    return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
}

可以看到系统默认过滤掉 static、transient 声明的字段,将相关字段 Field 包装在 ObjectStreamField ,ObjectStreamField 是对 Field 的封装,简单看下它的构造方法:

 ObjectStreamField(Field field, boolean unshared, boolean showType) {
    //当前字段
    this.field = field;
    //是否可以被序列化
    this.unshared = unshared;
    //字段名称
    name = field.getName();
    //字段类型
    Class<?> ftype = field.getType();
    //是基本类型还是引用类型
    type = (showType || ftype.isPrimitive()) ? ftype : Object.class;
    //获取字段类型的签名
    signature = getClassSignature(ftype).intern();
}

那该如何理解 serialPersistentFields 自行实现字段序列化规则呢?我们还是通过上面的的示例来了解下:

  • serialPersistentFields 自行实现字段序列化规则

      public final class Person implements Serializable {
    
      private String name;
      private int age;
      private String like;
    
      private transient String features;
    
      private static final ObjectStreamField[] serialPersistentFields = {
              new ObjectStreamField("name", String.class),
              new ObjectStreamField("age", Integer.class),
              new ObjectStreamField("features", String.class)
      };
    

    }

对 Person 做了下简单修改,添加 serialPersistentFields 字段,并指定了要序列化的字段名称,前面有分析道 Serializable 默认序列化规则会忽略 static、transient 声明的字段。此时我们看下自行实现字段序列化规则,看下它是如何实现的:

private static ObjectStreamField[] getDeclaredSerialFields(Class<?> cl)
        throws InvalidClassException {
    ObjectStreamField[] serialPersistentFields = null;
    try {
        //通过反射serialPersistentFields判断是否声明自定义字段序列化规则
        Field f = cl.getDeclaredField("serialPersistentFields");
        int mask = Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL;
        if ((f.getModifiers() & mask) == mask) {
            //必须声明为private static final
            f.setAccessible(true);
            serialPersistentFields = (ObjectStreamField[]) f.get(null);
        }
    } catch (Exception ex) {
    }
    if (serialPersistentFields == null) {
        return null;
    } else if (serialPersistentFields.length == 0) {
        return NO_FIELDS;
    }

    //保存需要序列化的Field字段
    ObjectStreamField[] boundFields =
            new ObjectStreamField[serialPersistentFields.length];
    Set<String> fieldNames = new HashSet<>(serialPersistentFields.length);
    for (int i = 0; i < serialPersistentFields.length; i++) {
        ObjectStreamField spf = serialPersistentFields[i];
        //获取字段名称
        String fname = spf.getName();
        if (fieldNames.contains(fname)) {
            //不能存在重复的字段名
            throw new InvalidClassException(
                    "multiple serializable fields named " + fname);
        }
        //不允许存在重复的字段名称
        fieldNames.add(fname);

        //大量反射
        try {
            //根据字段名称获取对应的Field
            Field f = cl.getDeclaredField(fname);
            //自定义传递字段类型必须与实际类型一致
            if ((f.getType() == spf.getType())
                    //不是static声明字段
                    && ((f.getModifiers() & Modifier.STATIC) == 0)) {

                boundFields[i] =
                        new ObjectStreamField(f, spf.isUnshared(), true);
            }
        } catch (NoSuchFieldException ex) {
        }
        if (boundFields[i] == null) {
            //反射获取第i个字段失败
            boundFields[i] = new ObjectStreamField(
                    fname, spf.getType(), spf.isUnshared());
        }
    }
    return boundFields;
}

serialPersistentFields 属性可以帮助我们替换默认字段序列化流程,ObjectStreamField 类型数组定义了需要被序列化的字段,而且该规则将会替代默认字段序列化规则声明,否则将不会被自动序列化。

此时 transitent 声明字段也可以被添加进序列化规则,但是 static 声明字段仍然不可以:

((f.getModifiers() & Modifier.STATIC) == 0)
  • 关于 serialPersistentFields 属性简单做下总结:
  1. serialPersistentFields 必须用 private static final 声明;
  2. 该属性会替换默认字段(非static、非transient)序列化规则,此时需要序列化字段必须添加到属性中;
  3. 被 transient 声明字段也可以被添加进序列化中。

serialPersistentFields 相对来说提供的“支持”有限,比如我们我们要将相关敏感数据序列化之前先经过加密,反序列时自动对其解密,该如何更好的实现呢?writeObject() 和 readObject() 这两个方法就可以帮我们一次性满足。

  • writeObject() 和 readObject() 方法作用

还是通过上面的例子,添加 writeObject() 和 readObject() 自定义序列化/反序列流程:

public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException {
        //对age字段序列化之前进行加密
        age = age << 2;
        //继续使用默认序列化流程
        stream.defaultWriteObject();
        //也可以直接
        //stream.writeInt(age);
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        //也可以使用
        //stream.readInt();
        //反序列化后对其解密
        age = age << 2;
    }
}

通过自行实现序列化 writeObject 和 反序列化 readObject 方法替换默认流程,我们可以对某些字段做些特殊修改,也可以实现序列化的加密功能。

如果此时我们序列化对象的类型发生了变化时该如何处理呢?这时我们可以通过 writeReplace 和 readResolve 方法实现对序列化的版本兼容。接下来我们就聊聊 wirteReplace 和 readResolve 方法的作用。

  • writeReplace 和 readResolve 方法作用

还是通过上面的例子,做下改造进行说明:

    public final class Person implements Serializable {

    private String name;
    private int age;
    private String like;

    private Person spouse;

    public Person(String name, int age, String like) {
        this.name = name;
        this.age = age;
        this.like = like;
    }

    private Object writeReplace()
            throws java.io.ObjectStreamException {
        return new PersonProxy(this);
    }

    public void setSpouse(Person value) {
        spouse = value;
    }

    public Person getSpouse() {
        return spouse;
    }

    //代理类
    private class PersonProxy implements Serializable {

        private String data;

        public PersonProxy(Person person) {
            this.data = person.name + "," + person.age + "," + person.like;

            final Person spouse = person.spouse;
            if (spouse != null) {
                data = data + "," + spouse.name + "," + spouse.age + "," + spouse.like;
            }
        }

        private Object readResolve()
                throws java.io.ObjectStreamException {
            String[] pieces = data.split(",");

            final Person result = new Person(pieces[0], Integer.parseInt(pieces[2]), pieces[1]);
            if (pieces.length > 3) {
                result.setSpouse(new Person(pieces[3], Integer.parseInt
                        (pieces[5]), pieces[4]));
                result.getSpouse().setSpouse(result);
            }
            return result;
        }
    }
}

writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。

这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。

  • Serializable 的序列化/反序列化的调用流程如下:

      //序列化
      E/test:SerializableTestData writeRepace
      E/test:SerializableTestData writeObject
      
      //反序列化
      E/test:Serializable readObject
      E/test:Serializable readResolve
    

分析到这里不知道你是否有感受到,Serializable 整个计算过程非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。

重新回到 ObjectOutputStream writeObject0 方法中,接着向下分析(详细方法体已在上面贴出),在拿到 ObjectStreamClass 实例后,首先判断是否有自行实现的 writeReplace、并且该方法返回不为 null 与不是当前对象类型:

        if (desc.hasWriteReplaceMethod()
                && (obj = desc.invokeWriteReplace(obj)) != null
                && (repCl = obj.getClass()) != cl) {
            //如果实现了writeReplace方法替换掉原写入对象 与
            //该方法返回对象不为null 与
            //返回对象类型不是当前序列化类型
            cl = repCl;
            //生成替换对象类型的ObjectStreamClass
            desc = ObjectStreamClass.lookup(cl, true);
        }

这里大家可以结合上面对 ObjectStreamClass 构造方法和示例进行理解。

接着最后 writeOrdinaryObject 方法(这里只看主线流程):

    private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared) throws IOException {
    if (extendedDebugInfo) {
        debugInfoStack.push(
            (depth == 1 ? "root " : "") + "object (class \"" +
            obj.getClass().getName() + "\", " + obj.toString() + ")");
    }
    try {
        //检查ObjectStreamClass对象
        desc.checkSerialize();
        //写入字节0x73
        bout.writeByte(TC_OBJECT);
        //写入class信息
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            //是否实现了Externalizable与不是代理类型
            writeExternalData((Externalizable) obj);
        } else {
            //写入该对象变量信息及其父类的成员变量
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}

checkSerialize 方法是检查之前一系列流程是否发生过异常,否则将抛出异常。

这里我们重点跟踪下 writeSerialData 方法:

   private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException {

    //获取当前类以及其父类所有实现Serializable的ObectStreamClass
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        //判断是否重写了writeObject()方法
        //还记得在ObjectStreamClass构造方法中判断当前类中是否定义了writeObject方法,
        //判断writeObject method 是否为null
        if (slotDesc.hasWriteObjectMethod()) {
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;

            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class \"" +
                    slotDesc.getName() + "\")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                //反射调用重写过的writeObject()方法
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }

            curPut = oldPut;
        } else {
            //否则走默认写入规则
            defaultWriteFields(obj, slotDesc);
        }
    }
}

注意看 for 循环遍历所有的 ObjectStreamClass , 根据是否自行实现了 writeObject 方法走自行实现字段序列化规则还是走默认序列化规则 defaultWriteFields 方法,关于自定义 writeObject 方法序列化规则,上面通过示例做过分析。下面我们主要看下默认序列化规则 defaultWriteField 方法:

    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
    throws IOException {
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        //obj不是cl类型实例
        throw new ClassCastException();
    }
    //检查中间过程是否有非法信息
    desc.checkDefaultSerialize();

    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    desc.getPrimFieldValues(obj, primVals);
    bout.write(primVals, 0, primDataSize, false);
    //获取当前类的所有字段信息
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    //返回获取每个Field的值,保存在objVals中
    //这里就是通过反射获取到Field在当前obj中对应的值
    desc.getObjFieldValues(obj, objVals);
    for (int i = 0; i < objVals.length; i++) {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                "field (class \"" + desc.getName() + "\", name: \"" +
                fields[numPrimFields + i].getName() + "\", type: \"" +
                fields[numPrimFields + i].getType() + "\")");
        }
        try {
            //根据实际类型调用
            //这里会递归调用writeObject0方法,
            //writeObject0方法的最后根据实际类型写入
            //如果是引用类型,又会重复该过程
            writeObject0(objVals[i],
                         fields[numPrimFields + i].isUnshared());
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
}

该类的所有字段已经保存在 ObjectStreamClass 中,这里也是直接获取到所有 Field,然后通过反射获取到该 Field 在当前 obj 中的值,然后根据实际类型调用 writeObject0 方法,注意如果是引用类型,又会递归调用该过程。

    if (obj instanceof Class) {
        writeClass((Class) obj, unshared);
    } else if (obj instanceof ObjectStreamClass) {
        writeClassDesc((ObjectStreamClass) obj, unshared);
    // END Android-changed
    } else if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        //我们只看实现 Serializable 序列化过程
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }

以写入 writeString 方法为例看下:

private void writeString(String str, boolean unshared) throws IOException {
    handles.assign(unshared ? null : str);
    long utflen = bout.getUTFLength(str);
    if (utflen <= 0xFFFF) {
        bout.writeByte(TC_STRING);
        bout.writeUTF(str, utflen);
    } else {
        bout.writeByte(TC_LONGSTRING);
        bout.writeLongUTF(str, utflen);
    }
}

实际先写入到 BlockDataOutputStream 缓存中(ObjectOutputStream 构造方法中创建,默认缓存大小是 1KB),每当写入内容超过该阈值:

public void write(int b) throws IOException {
        if (pos >= MAX_BLOCK_SIZE) {
            //超过最大最大可写入此时要强制写入
            //outputStream
            drain();
        }
        buf[pos++] = (byte) b;
}

void drain() throws IOException {
        if (pos == 0) {
            return;
        }
        if (blkmode) {
            writeBlockHeader(pos);
        }
        //写入到outputStream中
        out.write(buf, 0, pos);
        pos = 0;
        warnIfClosed();
    }

此时调用的就是传入 ObjectOutputStream 构造方法的输出流了。

分析到这里,关于 Serializable 序列化背后的实现原理就已经分析完了,当然文中还省去了大部分实现细节,感兴趣的朋友可以进一步阅读源码进行分析。

Java 提供的 Serializable 对象序列化机制,远比大多数 Java 开发人员想象的更灵活,这使我们有更多的机会解决棘手的情况。其实像这样的编程妙招在 JVM 中随处可见。关键是我们要知道它们,在后续的项目中可以更好的实践它们。

总结

  • Serializable 序列化支持替代默认流程,它会先反射判断是否存在我们自己实现的序列化方法 writeObject 或 反序列化方法 readObject。通过这两个方法,我们可以对某些字段做一些特殊修改,也可以实现序列化的加密功能。

  • 我们可以通过 writeReplace 和 readResolve 方法实现自定义返回的序列化实例。通过它们实现对序列化的版本兼容,例如通过 readResolve 方法可以把老版本的序列化对象转换成新版本的对象类型。

  • Serializable 整个序列化过程使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化引用的其它对象。

Serializable 使用注意事项
  • 不被序列化字段。类的 static 变量以及被声明为 transient 的字段,默认的序列化机制都会忽略该字段,不会进行序列化存储。当然我们也可以使用进阶的 writeObject 和 readObject 方法做自定义的序列化存储。

  • serialVersionUID。在类实现了 Serializable 接口后,我们需要添加一个 Serial Version ID,它相当于类的版本号。这个 ID 我们可以显示声明也可以让编译器自己计算。通常建议显示声明会更加稳妥,因为隐士声明假如类发生了一点点变化,进行反序列化都会由于 serialVersionUID 改变而导致 InvalidClassException 异常。

  • 构造方法。Serializable 的反序列化默认是不会执行构造函数的,它是根据数据流中对 Object 的描述信息创建对象的。如果一些逻辑依赖构造函数,就可能会出现问题,例如一个静态变量只在构造方法中赋值,当然我们也可以通过进阶的自定义反序列化修改。

虽然 Serializalbe 性能那么差,但是它也有一些进阶的使用技巧。不过在 Android 需要重新设计一套更加轻量且高效的机制,感兴趣可以继续阅读该系列下篇文章《Android 对象序列化之 Parcelable 取代 Serializable ?

推荐参考资料

以上便是个人在学习 Serializable 时相关心得和体会,文中如有不妥或有更好的分析结果,欢迎指出。

文章如果对你有帮助,就请留个赞吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343