从序列化和反序列化看 Unity3D 的存储机制

一、序列化与反序列化的基本使用

1.1 概念

序列化: 将对象转为二进制数据的过程。
反序列化: 将二进制数据转为对象的过程。

1.2 使用

1.2.1 序列化(Serialize)

实现原理: 利用“反射”拿到对象类型的元数据,进而了解要序列化的对象的信息(不区分访问权限地查看所有字段),然后转换为二进制数据写入流中。
实现步骤:

  1. 获取对象类型中的所有字段信息;
  2. 获取对象中的成员所对应的值;
  3. 将程序集标识以及类型的完整名称写入流中;
  4. 将1,2步骤中的数据写入流中。

具体操作步骤:

  1. 定义一个 Stream 的派生类对象(Stream 为抽象类);
  2. 定义一个序列化格式化器,例如 BinaryFormatter;
  3. 利用BinaryFormatter.Serialize(Stream serializationStream, object graph); 进行序列化;

示例:

MemoryStream memoryStream = new MemoryStream();
BinaryFormatter binaryFormmater = new BinaryFormatter();
binaryFormmater.Serialize(memoryStream, obj);

1.2.2 反序列化(Deserialize)

实现原理: 通过拿到流中的程序集标识、类型名称、字段名等数据实例化对象以返回。
实现步骤:

  1. 拿到流中的程序集标识、类型名称获取对象类型,即System.Type类型的对象;
  2. 在内存上分配对应类型的内存空间(此时不会调用构造函数);
  3. 获取对象类型的字段数组;
  4. 获取对象各个字段的值;
  5. 根据字段与字段对应的值为新分配的对象进行初始化。

具体操作步骤:

  1. 定义一个序列化格式化器,例如 BinaryFormatter;
  2. 利用binaryFormatter.Deserialize(Stream serializationStream); 进行序列化;

示例:

BinaryFormatter binaryFormatter = new BinaryFormatter();
stream.Position = 0; // 将 stream 定位置位
return binaryFormatter.Deserialize(stream);

1.3 总结

  1. Stream对象的作用主要是为了给序列化得到的字节块提供容器;
  2. 同一个流可以存放多个对象序列化得到的字节块并可以保证反序列化成功;
  3. 序列化与反序列化同一个对象时必须保证使用同一个格式化器,否则会因为无法解释流中内容而抛出错误。

二、控制序列化与反序列化

2.1 使类型可被序列化

概念: 由于类型默认是无法被序列化的,因此如果想要序列化自定义类型,则需要添加[Serializable]特性。
示例:

[Serializable]
class Hero
{
    public int age;
    public string name;
}

2.2 控制字段是否被序列化和控制反序列化过程

概念: 通过对具体的字段/函数添加特性以进一步控制序列化与反序列化。
示例:

[Serializable]
class Hero
{
    [NonSerialized]
    public int age; // 此时age不会被序列化进流中,因此反序列化得到的Hero实例的age为默认值(0)
    // ... ...
}
// 建议均声明为 private 权限以提高代码安全性
[OnSerializing]
private void SetName(StreamingContext streamingContext)
{
    Debug.Log("在序列化字段前被调用");
}
[OnSerialized]
private void SetIdOnSerialized(StreamingContext streamingContext)
{
    Debug.Log("在序列化字段后被调用");
}
// 建议均声明为 private 权限以提高代码安全性
[OnDeserializing]
private void SetId(StreamingContext streamingContext)
{
    Debug.Log("反序列化字段前被调用");    
    Debug.Log($"SetId id = {this.id}"); // 此时 id 为 0
    this.id = 1002; // 这里赋的值会在真正的反序列化字段时被覆盖
}
[OnDeserialized]
private void SetAge(StreamingContext streamingContext)
{
    Debug.Log("反序列化字段后被调用");  
    this.age = 18; // 这里赋的值会覆盖反序列化对字段的赋值
}

注意:

  1. [Serializable]特性是不能被派生类继承的,即子类如果想要序列化,则也需要添加特性;
  2. [NonSerialized]特性是能被派生类继承的;
  3. 若派生类添加了[Serializable]特性,但基类没有添加,此时依旧无法被序列化。例如C#中所有类型的基类System.Object早已应用了[Serializable]特性。

2.3 流的上下文介绍及应用

结构:

属性 类型 作用
State StreamingContextStates 用来说明要序列化和反序列化的对象的来源于目的地
Context Object 一个上下文对象的引用,包含了用户希望得到的任何上下文信息

意义: 通过State属性的值描述给定的序列化流的源和目标,并利用Context属性提供一个由调用方定义的附加上下文。
使用: 默认情况下,State会被设置为 All,Context会被设置为 Null。
注意:

  1. 如果序列化自定义类型时未添加[Serializable]特性,尝试序列化时会报错:SerializationException: Type '...+Hero' in ... is not marked as serializable.;
  2. 如果控制序列化/反序列化过程中未访问有关序列化的上下文信息则可以不添加参数 StreamingContext;

三、Unity3D内部的序列化与反序列化操作

3.1 具体表现

  1. 预制体Prefab
    1. Prefab:Unity内部的Prefab就是对游戏对象或组件进过序列化后得到的文件,即可以是YMAL文件也可以是Binary文件;
    2. 实例化:在调用Instantiate函数时通过对Object进行序列化与反序列化,来达到深层拷贝的目的;
    3. 存储场景:场景的存储方式与Prefab类似,也是通过序列化来保存的;
    4. 载入场景:Unity中载入场景是通过读取场景文件并对其进行反序列化实现的;
    5. 重载编辑器代码:当开发人员修改编辑器代码后,旧有的编辑器窗口的数据会序列化,Unity3D会加载新的编辑器窗口并将旧窗口数据反序列化以提供给新窗口使用。
    6. Resource.GarbageCollectSharedAssets()方法,这是Unity3D所提供的垃圾回收机制。当新场景加载完成后会查找出上一个场景中存在,但在新场景不存在的游戏物体并进行卸载。这也是利用了序列化的机制,来获取所有有外部引用的对象(UnityEngine.Object类型)。依据这个机制,就能在新场景加载完毕后对旧场景中的对象进行卸载。
  2. Inspector面板
    1. 表现:通过对象的反序列化得到面板中显示的属性值。

3.2 对Unity3D游戏脚本进行序列化的注意事项

  1. 什么样的字段可以被序列化:
    1. 访问权限为public,或者使用了[SerializeField]特性;
    2. static字段;
    3. const字段;
    4. readonly字段;
    5. 字段类型必须是可被序列化的。
  2. 什么样的类型可以被序列化:
    1. 自定义非抽象类,且必须使[Serializable]特性;
    2. 自定义结构体,且必须使[Serializable]特性;
    3. 所有派生自UnityEngine.Object类的类型;
    4. C#基本类型,如 int、float、double、bool、string等;
    5. 以上四种类型数组Array 或 列表List。


注:本篇文章为阅读《Unity3D脚本编程》后的一篇个人学习笔记,如果想要了解更多内容建议阅读原著。文章中如果有不准确的地方,欢迎各位留言提醒我来改正,避免误导他人。

你可能感兴趣的:(笔记,Unity,unity,c#)