Unity中的序列化与反序列化

Unity的数据存储,本地类 PlayerPrefs, Inspector,以及Prefab等都使用了序列化与反序列化的知识.
循序渐进,让我们一步步了解Unity中的序列化和反序列化的知识;

流与格式化器

序列化: 将对象转换为字节流.
反序列化: 将字节流转换为对象.
直接讲概念太抽象,我们先来看代码;

using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class Test : MonoBehaviour
{
    private void Start()
    {
        Hero hero_ins = new Hero();
        hero_ins.id = 100;
        hero_ins.attack = 99f;
        hero_ins.defence = 99f;
        hero_ins.name = "Calabash";

        Stream st = FormatInstanceToMemory(hero_ins);
        st.Position = 0;
        hero_ins = null;

        hero_ins = MemoryToInstance(st) as Hero;
        Debug.Log(hero_ins.id.ToString());
        Debug.Log(hero_ins.attack.ToString());
        Debug.Log(hero_ins.defence.ToString());
        Debug.Log(hero_ins.name);
    }

    //序列化 把实例对象写入流中
    private static MemoryStream FormatInstanceToMemory(object instance)
    {
        //创建一个流
        MemoryStream ms = new MemoryStream();
        //创建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化为二进制流
        bf.Serialize(ms, instance);

        return ms;
    }
    //反序列化, 从流中读出实例对象
    private static object MemoryToInstance(Stream st)
    {
        //创建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //把二进制流反序列化为指定的对象
        return bf.Deserialize(st);
    }

}

关于Hero类的定义如下:

[Serializable] //注意这个关键字
public class Hero
{
    public int id;
    public float attack;
    public float defence;
    public string name;
}

代码中的注释已经写得很清楚了,通过代码我们要解释三个概念;

  • 流(Stream): Unity中的二进制数据流,有 MemoryStream, FileStream 等子类来处理不同场景的数据流,但我们这里不讨论每种流的用法,只需要让大家理解 Stream提供了一个用来容纳经过序列化之后的字节块的容器
    更多的Stream知识可以查阅这里: Unity的Stream流
  • 格式化器: 使用序列化和反序列的工具,代码中只是使用到了 BinaryFormatter 这种格式化器,其实还有 SoapFormatter (需要导入对应的.dll文件),需要注意的是进行序列化和反序列的操作必须是相同的格式化器,否则可能会抛出System.Runtime.Serialization.SerializationException异常.
  • [Serializable]特性: 默认自定义的类型是无法被序列化的,需要使用 [Serializable] 特性来实现序列化与反序列化,关于此特性更多的内容见下节;

通过上面的示例,我们往流中写入了一个对象,那么可以写入两个,甚至多个不同的对象么?答案是肯定的,我们还是用代码测试一下;

using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class Test : MonoBehaviour
{
    private void Start()
    {
        Hero hero_ins = new Hero();
        hero_ins.id = 100;
        hero_ins.attack = 99f;
        hero_ins.defence = 99f;
        hero_ins.name = "Calabash";

        Soldier soldier_ins = new Soldier();
        soldier_ins.life = 50;
        soldier_ins.weapon = "hammer";

        //创建一个流
        MemoryStream ms = new MemoryStream();
        //创建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化为二进制流
        bf.Serialize(ms, hero_ins);
        bf.Serialize(ms, soldier_ins);

        ms.Position = 0;
        hero_ins = null;
        soldier_ins = null;

        //从数据流中读出数据
        //读出的顺序不能颠倒,因为是从ms的开端读取,因此要按写入的顺序读取
        hero_ins = bf.Deserialize(ms) as Hero;
        soldier_ins = bf.Deserialize(ms) as Soldier;


        Debug.Log("hero: " + hero_ins.id.ToString());
        Debug.Log("hero: " + hero_ins.attack.ToString());
        Debug.Log("hero: " + hero_ins.defence.ToString());
        Debug.Log("hero: " + hero_ins.name);
        Debug.Log("soldier: " + soldier_ins.life.ToString());
        Debug.Log("soldier: " + soldier_ins.weapon);
    }

[Serializable]与[NonSerialized]的继承

1. [Serializable]

该特性只能用于以下类型:

  • 引用类型(class)
  • 值类型(struct)
  • 枚举类型(enum)
  • 委托类型(delegate)

该特性不会被派生的子类继承;

[Serializable] //注意这个关键字
public class Hero
{
    public int id;
    public float attack;
    public float defence;
    public string name;
}


public class GirlHero : Hero
{
    public int girlAge;
}
public class Test : MonoBehaviour
{

    private void Start()
    {
        Hero hero_ins = new Hero();
        hero_ins.id = 100;
        hero_ins.attack = 99f;
        hero_ins.defence = 99f;
        hero_ins.name = "Calabash";

        GirlHero girl_ins = new GirlHero();
        girl_ins.girlAge = 18;

        //创建一个流
        MemoryStream ms = new MemoryStream();
        //创建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化为二进制流
        bf.Serialize(ms, hero_ins);
        bf.Serialize(ms, girl_ins);

        ms.Position = 0;
        hero_ins = null;
        girl_ins = null;

        //从数据流中读出数据
        //读出的顺序不能颠倒,因为是从ms的开端读取,因此要按写入的顺序读取
        hero_ins = bf.Deserialize(ms) as Hero;
        girl_ins = bf.Deserialize(ms) as GirlHero;


        Debug.Log("hero: " + hero_ins.id.ToString());
        Debug.Log("hero: " + hero_ins.attack.ToString());
        Debug.Log("hero: " + hero_ins.defence.ToString());
        Debug.Log("hero: " + hero_ins.name);
        Debug.Log("girl: " + girl_ins.girlAge.ToString());

    }
}

点击运行后,果不其然会报 SerializationException 的一个错误:

SerializationException: Type 'GirlHero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

错误信息很明显,我们的 GirlHero 类没有标记 Serializable 特性,当我们给这个类也标记上该特性后,结果可以正常被打印;

我们在来看另外一种情况,只有派生类使用特性,基类不使用:

public class Hero
{
    public int id;
    public float attack;
    public float defence;
    public string name;
}

[Serializable]
public class GirlHero : Hero
{
    public int girlAge;
}

运行后报错如下:

SerializationException: Type 'Hero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

通过这个测试,我们可以知道:

  1. Serializable特性不会被继承,每个派生类如果要想被序列化,需要单独添加此特性字段.
  2. 派生类添加了 Serializable 特性,而基类不使用,那么从基类派生的任何类都无法被序列化.
    可以这么理解,基类如果无法被序列化,那么它的字段无法被序列化,派生类同样包含该基类的字段,那么自然也是无法被序列化的.C#中的所有类都是继承自 System.Object 类,这个类已经应用了 Serializable 特性.

2. [NonSerialized]

在默认情况下,序列化会读取对象的所有字段,无论这些字段生命的访问权限是 public 还是 private, 如果我们有些敏感字段或者计算属性不想被序列化,有没有办法呢?
在不想被序列化的字段上面使用 NonSerialized 属性即可;

[Serializable] //注意这个关键字
public class Hero
{
    public int id;
    [NonSerialized]
    public float attack;
    public float defence;
    public string name;
    
}

使用上面Test的脚本,打印结果如下:

NonSerialized特性的使用

我们可以看到反序列化后,被标记为 NonSerialized 特性的字段值变为了0,这是由于
attack 字段不能被序列化,它的值99并不会写入到流中,因此被反序列化后,其余字段都能够被正常赋值,该字段由于从流中读取不到对应的值,只能设置为0;
那么能不能在反序列化的时候,把正确的值赋值回去呢?答案是肯定的,我们下节再来解决这个问题,我们继续查看 NonSerialized 的继承特点;

[Serializable] //注意这个关键字
public class Hero
{
    public int id;
    [NonSerialized]
    public float attack;
    public float defence;
    public string name;
    
}

[Serializable]
public class GirlHero : Hero
{
    public int girlAge;
}
public class Test : MonoBehaviour
{

    private void Start()
    {
        GirlHero girl_ins = new GirlHero();
        girl_ins.id = 100;
        girl_ins.attack = 99f;
        girl_ins.defence = 99f;
        girl_ins.name = "Calabash";
        girl_ins.girlAge = 18;

        //创建一个流
        MemoryStream ms = new MemoryStream();
        //创建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化为二进制流
        bf.Serialize(ms, girl_ins);

        ms.Position = 0;
        girl_ins = null;

        //从数据流中读出数据
        girl_ins = bf.Deserialize(ms) as GirlHero;

        Debug.Log("girl_id: " + girl_ins.id.ToString());
        Debug.Log("girl_attack: " + girl_ins.attack.ToString());
        Debug.Log("girl_defence: " + girl_ins.defence.ToString());
        Debug.Log("girl_name: " + girl_ins.name);
        Debug.Log("girl_age: " + girl_ins.girlAge.ToString());

    }
}

打印结果如下:

NonSerialized特性可以被继承

通过上面的测试可以得知: [NonSerialized] 特性可以被派生类继承;


控制序列化和反序列化的流程

在上一节提出的问题,对于 NonSerialized 修饰的字段,在反序列化的时候应该如何赋值,以及如果我们想在序列化和反序列化之前和之后做些操作,应该怎么实现?

[Serializable]
public class GirlHero : Hero
{
    public int girlAge;

    [OnDeserialized]
    private void CaculateAttack(StreamingContext context)
    {
        this.attack = 1000;
    }
}

在上一节的代码基础上,我们对 GirlHero 做了上面的改动,增加了一个 CaculateAttack 方法,并且使用了 [OnDeserialized] 特性,我们再来看打印结果:

控制反序列化

通过这样的方法和特性我们对 attack 字段在反序列化的时候进行了赋值;
从特性的名字可以看出,是在反序列化过程完成后调用所修饰的方法,还有其他三个相关特性我们一起来看看;

1. 序列化与反序列化过程的方法特性

  • OnSerializing :格式化器在序列化对象字段之前,调用该特性修饰的方法.
  • OnSerialized :格式化器在序列化对象字段之后,调用该特性修饰的方法.
  • OnDeserializing :格式化器在反序列化对象字段之前,调用该特性修饰的方法.
  • OnDeserialized ::格式化器在反序列化对象字段之后,调用该特性修饰的方法.

这几个特性是在 System.Runtime.Serialization 命名空间下,共同点是用来修饰类型中定义的方法;注意他们的调用时机.

2. StreamingContext

在上面的实例代码中,可以看到方法参数是一个 StreamingContext 类,这个类是序列化与反序列化时流的上下文,我们通过程序集可以看到,这个类型是一个值类型.

public struct StreamingContext
{
    //调用方定义的附加上下文引用,一般为空
    public object Context {
        get;
    }
    //用来标记序列化和反序列对象的来源和目的地 
    public StreamingContextStates State {
        get;
    }
    //构造方法
    public StreamingContext (StreamingContextStates state);

    public StreamingContext (StreamingContextStates state, object additional);
    //重载System.Object方法
    public override bool Equals (object obj);

    public override int GetHashCode ();
}

通过State的属性我们可以查看序列化和反序列化时对应的来源和目的地,更多的信息请查阅这里:StreamingContextStates枚举
我们在上面序列化时使用的格式化器的 Context 属性就是 StreamingContext, 它的 State 属性默认是All,我们也可以在创建格式化器的时候手动指定 State 的类型来满足不同的需求,比如:

//指定state类型,深度克隆一个对象
BinaryFormatter bf = new BinaryFormatter();
bf.Context = new StreamingContext(StreamingContextStates.Clone);

Unity的Inspector

在属性监视板中可以看到游戏脚本中某个对象的信息,这些字段和值并不是Unity调用游戏脚本中的C#接口获取的,而是通过显示对象的反序列化得到这些属性数值,然后在面板中展示出来;


Unity的Prefab

Prefab是Unity中很重要的一种资源类型,真正实现了游戏对象的克隆,预制体是游戏对象和组件经过序列化后得到的文件,它的格式可以是二进制的也可以是文本文件,可以通过下面的选项来设置:

资源格式设置

它的特点如下:

  • 可以被放入多个场景中,也可以在一个场景中放入多个
  • 在场景中增加一个Prefab,就实例化了一个该Prefab的实例
  • 所有的Prefab实例都是Prefab的克隆,因此在运行中生成Prefab实例的话可以看到这些实例会带有(Clone)的标记
  • 只要Prefab的原型发生了变化,场景中所有的prefab实例都会发生变化

脚本创建Prefab实例我们都是通过Instantiate方法:

public static Object Instantiate (Object original, Vector3 position, Quaternion rotation)

在该方法内部,会首先将参数original所引用的游戏对象序列化,得到序列化流后,再使用反序列化机制将这个序列化流生成一个新的游戏对象,可以说是对象的克隆操作;


Unity在System.Runtime.Serialization命名空间下定义了一个FormatterServices的类型,只包含一些静态方法,用来辅助序列化与反序列化的过程;

序列化过程

  1. 调用FormatterServices的 GetSerializableMembers ;
//两个重载版本
//type: 正在序列化或克隆的类型
//context: 发生序列化的上下文
//MemberInfo[]: 返回类型对象的数组,每一个元素都对应一个可以成员字段的名称
public static MemberInfo[] GetSerializableMembers(Type type, StreamingContext context)
public static MemberInfo[] GetSerializableMembers(Type type)
  1. 调用FormatterServices的 GetObjectData ;
//obj: 表示要写入序列化程序的对象实例
//members: 代表的是第一步提取的成员字段的名称
//Object[]: 返回的是对应members中每个元素表示的字段对应的值,理解为Value的集合
public static Object[] GetObjectData(Object obj, MemberInfo[] members)
  1. 经过前两个步骤获取了对象的成员和其对应的值,这一步先把程序集标识以及类型的完整名称写入流中.
  2. 格式化器遍历第一步与第二步得到的数组获取成员名称和其对应的值,将这些信息写入流中.

反序列化过程

  1. 格式化器从流中读取程序集标识和完整的类型名称,然后调用FormatterServices的 GetTypeFromAssembly ;
//assem: 读取到的程序集标识
//name: 完整的类型名称
//Type: 返回值便是反序列化对象的实际类型
public static Type GetTypeFromAssembly(Assembly assem, string name)
  1. 获取了对象的类型后,接下来就是要在为新的对象分配一块内存空间,调用FormatterServices的 GetUninitializedObject ;
//为指定类型分配内存空间
public static Object GetUninitializedObject(Type type)

需要注意的是,此时还没有调用构造函数,对象的所有字节都被初始化为 null 或者 0 ;

  1. 分配好内存空间后,还是调用FormatterServices的 GetSerializableMembers 构造并初始化一个新的 MemberInfo 数组;
    这个方法的说明见序列化过程的第一步;
    调用方法后获取该类型的所有成员字段名称的集合 MemberInfo[] members ;
  2. 获取到字段信息后,这一步就要获取字段对应数组的信息;格式化器会根据流中包含的数据创建一个 Object 数组,对其进行初始化;
    到了这一步,你就有了一个未初始化的对象,一个成员变量集合和对应数值的集合;
  3. 这一步就要调用FormatterServices的 PopulateObjectMembers 方法对实例对象初始化;
//obj: 表示刚才创建要被初始化的对象实例
//members: 对象需要被填充的成员或者属性
//data: 对象需要被填充的成员或者属性对应的数值
//Object: 返回一个初始化好的实例对象
public static Object PopulateObjectMembers(Object obj, MemberInfo[] members, Object[] data)

参考文章: <> 陈嘉栋

你可能感兴趣的:(Unity中的序列化与反序列化)