什么是序列化和反序列化:序列化和反序列化主要用来解决对象的传输问题。其中序列化用来将对象转换成二进制流;反序列化用来将二进制流转换成对象。
Unity序列化:Unity序列化器的实现十分简单,目的是为了非常快速的进行序列化和反序列化操作。具有以下特性:
1.可以被序列化的类型如下所示:
1.1.某些Unity内置类型。如:Vector2、Vector3、Vector4、Rect、Quaternion、Matrix4x4、Color、Color32、LayerMask、AnimationCurve、Gradient、RectOffset、GUIStyle、UnityEvent。
1.2.某些C#基元类型。如:sbyte、byte、short、ushort、int、uint、long、ulong、float、double、char、string、bool。
1.3.某些基础枚举类型。在Unity5.6之前,只支持byte和int枚举类型。在Unity5.6之后,只支持sbyte、byte、short、ushort、int、uint枚举类型。
1.4.使用SerializableAttribute定制特性应用的自定义结构体类型。
1.5.继承自UnityEngine.Object的自定义引用类型。
1.6.使用SerializableAttribute定制特性应用的自定义引用类型,并且该自定义引用类型不能使用static、abstract修饰符。
1.7.某些容器类型。如:上述类型的一维数组、List类型。
2.当自定义结构体类型或者自定义引用类型实现了ISerializationCallbackReceiver接口的时,就可以在OnBeforeSerialize函数里面实现序列化之前的操作以及在OnAfterDeserialize函数里面实现反序列化之后的操作。
3.可以使用JsonUtility的ToJson函数来将可序列化类型的实例进行序列化成json格式串,也可以使用JsonUtility的FromJson或者FromJsonOverwrite函数来将json格式串反序列化成可序列化类型的实例。
4.类型中可以被序列化的成员只有字段。具有以下特性:
4.1.可以使用NonSerializedAttribute定制特性来让字段不能被序例化。
4.2.要想字段被成功的序列化,那么首先该字段就不能应用NonSerializedAttribute定制特性。接着该字段所对应的类型必须是可以序列化的类型。然后该字段不能使用static、const、readonly修饰符。最后该字段必须使用public访问权限或者SerializeFieldAttribute定制特性。
4.3.检视面板上会显示序列化成功的字段。
4.4.可以使用HideInInspectorAttribute定制特性来让序列化成功的字段在检视面板上面隐藏起来。
4.5.Unity序列化器不支持序列化多态情况。当字段接收子类实例赋值时,如果该字段可以被成功的序列化,那么该字段也就只能序列化基类部分。
4.6.Unity序列化器不支持序列化空引用数值情况。当字段在进行序列化时,如果该字段对应类型里面存在空引用数值,那么Unity序列化器首先就会通过该空引用数值对应类型的构造函数来创建一个实例;然后通过该实例来替换该空引用数值。
参考代码如下所示:
[Serializable]
public class NodeA
{
public NodeB nodeB = null;
}
[Serializable]
public class NodeB
{
public int value = 10;
}
public class MTest : MonoBehaviour
{
public NodeA nodeA;
}
空引用数值被具体实例替换的效果如下图所示:
4.7.Unity序列化器不支持序列化循环引用情况。当字段对应的类型存在循环引用自身类型时,为了避免无线循环下去,Unity就会限制最大循环深度为10。在达到这个最大循环深度之前,Unity会出现严重的卡顿。
参考代码如下所示:
[Serializable]
public class NodeA
{
public NodeB Cycle;
}
[Serializable]
public class NodeB : NodeA
{
}
public class MTest : MonoBehaviour
{
public NodeB Node;
}
深度限制警告如下图所示:
4.8.Unity序列化器不支持序列化字典类型情况。要想字典类型的字段被成功的序列化,那么就需要将字典类型的key和value各自存储成List类型来进行序列化。参考代码如下所示:
[Serializable]
public class UnitySerializedDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
[SerializeField, HideInInspector]
private List<TKey> keyData = new List<TKey>();
[SerializeField, HideInInspector]
private List<TValue> valueData = new List<TValue>();
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
this.Clear();
for (int i = 0; i < this.keyData.Count && i < this.valueData.Count; i++)
{
this[this.keyData[i]] = this.valueData[i];
}
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
this.keyData.Clear();
this.valueData.Clear();
foreach (var item in this)
{
this.keyData.Add(item.Key);
this.valueData.Add(item.Value);
}
}
}
4.9.可以使用FormerlySerializedAsAttribute定制特性来将旧字段的序列化数据映射到新的字段上。
5.交互层具有以下特性:
5.1.首先将C++ Native层中存储的序列化字段的数值进行反序列化并同步到C# 控制层。然后就可以在检视面板中看见该序列化字段的数值了。
5.2.如果在脚本文件中修改序列化字段的数值,那么该字段的修改数值即不会被序列化存储到C++ Native层,也不会被同步到C# 控制层。
5.3.如果在检视面板中修改序列化字段的数值,当游戏处于运行状态时,该字段的修改数值只能同步到C# 控制层;否则该字段的修改数值首先会序列化存储到C++ Native层,然后同步显示到C# 控制层。
6.可以使用AssetDatabase.ForceReserializeAssets函数来强制重新序列化指定路径下的所有资源。
Odin序列化:Odin序列化器主要用来扩展Unity序列化器。具有以下特性:
1.Odin内置序列化类型如下所示:
1.1.SerializedUnityObject:该类型表示通过Odin序列化器进行序列化的Unity Object。
1.2.SerializedScriptableObject :该类型表示通过Odin序列化器进行序列化的Unity ScriptableObject 。
1.3.SerializedComponent :该类型表示通过Odin序列化器进行序列化的Unity Component。
1.4.SerializedBehaviour:该类型表示通过Odin序列化器进行序列化的Unity Behaviour。
1.5.SerializedMonoBehaviour:该类型表示通过Odin序列化器进行序列化的Unity MonoBehaviour。
1.6.SerializedStateMachineBehaviour:该类型表示通过Odin序列化器进行序列化的Unity StateMachineBehaviour。
2.Odin自定义序列化类型的流程如下所示:
2.1.首先让类型继承自想要使用的Unity类型。
2.2.然后让类型实现ISerializationCallbackReceiver接口。
2.3.最后在OnBeforeSerialize函数里面使用UnitySerializationUtility类型的SerializeUnityObject函数来实现序列化,在OnAfterDeserialize函数里面使用UnitySerializationUtility.DeserializeUnityObject函数来实现反序列化。
3.使用Odin序列化器来序列化和反序列化任何类型实例的流程如下所示:
3.1.使用SerializationUtility类型的SerializeValue函数来对指定类型实例按照指定的格式进行序列化,并返回序列化后的字节流。
3.2.使用SerializationUtility类型的DeserializeValue函数来对序列化后的字节流按照指定的格式进行反序列化,并返回指定类型的实例。
3.3.SerializationUtility类型的SerializeValue函数和DeserializeValue函数当中用到的指定格式包含以下几种类型:
3.3.1.Binary:该格式一般在运行模式和构造下使用,主要用来将序列化的数据变成字节流。该格式不仅执行速度很快,而且还会将垃圾分配保持在最低限度。
3.3.2.Json:该格式主要用来将序列化的数据变成人类可以阅读的Json串。由于该格式没有做出任何性能优化,而且还会分配很多不必要的垃圾数据,因此建议还是优先使用性能更加高效的Unity JsonUtility来完成该操作。
3.3.3.Nodes:该格式一般在编辑器模式下使用,主要用来将序列化的数据变成可以和Unity文本资产序列化结合使用的节点列表。该格式的性能比Binary格式稍微差点。
3.4.SerializationUtility类型的SerializeValue函数中用到的指定SerializationContext类型参数以及SerializationUtility类型的DeserializeValue函数中用到的指定DeserializationContext类型参数当中都可以指定外部引用解析器。具有以下特性:
3.4.1.外部引用解析器主要用来对引用对象进行序列化和反序列化。
3.4.2.Odin提供了三种外部引用解析器接口,它们分别是IExternalStringReferenceResolver、IExternalGuidReferenceResolver、IExternalIndexReferenceResolver。
3.4.3.在IExternalStringReferenceResolver接口的CanReference函数当中判断是否将引用对象序列化成一个字符串标识。在IExternalStringReferenceResolver接口的TryResolveReference函数当中判断是否使用指定的字符串标识来反序列化成引用对象。
3.4.4.在IExternalGuidReferenceResolver接口的CanReference函数当中判断是否将引用对象序列化成一个Guid对象。在IExternalGuidReferenceResolver接口的TryResolveReference函数当中判断是否使用指定的Guid对象来反序列化成引用对象。
3.4.5.在IExternalIndexReferenceResolver接口的CanReference函数当中判断是否将引用对象关联到一个指定索引值的列表当中。在IExternalIndexReferenceResolver接口的TryResolveReference函数当中判断是否从列表当中获取指定索引的引用对象。
4.类型中可以被序列化的成员只有字段和属性。具有以下特性:
4.1.要想字段被成功的序列化,那么首先该字段所在的类型就必须继承自Odin内置序列化类型或者Odin自定义序列化类型。然后该字段不能使用static、const修饰符。最后该字段必须使用public访问权限或者SerializeFieldAttribute定制特性或者OdinSerializeAttribute定制特性。
4.2.要想属性被成功的序列化,那么首先该属性所在的类型就必须继承自Odin内置序列化类型或者Odin自定义序列化类型。然后该属性不能使用static修饰符。最后该属性必须使用SerializeFieldAttribute定制特性或者OdinSerializeAttribute定制特性。
4.3.检视面板上会显示序列化成功的字段和属性。
4.4.可以使用HideInInspectorAttribute定制特性来让序列化成功的字段和属性在检视面板上面隐藏起来。
4.5.由于Odin序列化器在执行序列化流程时,首先会将Unity不能序列化的字段和属性转换成Unity可以序列化的数据格式并存储在SerializationData字段中,然后Unity序列化器会将SerializationData字段中存储的数据进行序列化。这样一来,Odin序列化器的执行速度就会低于Unity序列化器。因此,建议优先使用Unity序列化器进行序列化和反序列化操作。
4.6.当不使用OdinSerializeAttribute定制特性来强制使用Odin序列化器时,如果可以使用Unity序列化器来序列化字段,那么就会忽略掉Odin序列化器的序列化操作。
4.7.当字段和属性既能被Unity序列化器进行序列化,也能被Odin序列化器进行序列化时,那么就会得到两份数据,进而会造成内存空间的浪费。
4.8.Odin序列化器在序列化字段和属性时,可以支持多态、空引用数值、循环引用、字典类型等情况。
4.9.可以使用PreviouslySerializedAsAttribute定制特性来将旧字段和属性的序列化数据映射到新的字段和属性上。但是,经过实践表明,该定制特性针对字段而言貌似不起作用。
5.可以使用BindTypeNameToTypeAttribute定制特性在不改变序列化数据的情况下将旧"命名空间.类型"重构成新"命名空间.类型"。
6.在AOT编译平台(如:iOS)上进行序列化时需要注意以下事项:
6.1.Unity的代码剥离设置项不仅无法为泛型变体生成必要代码,而且也会删除序列化系统所需要的某些类。
6.2.Odin AOT自动化功能会为指定的AOT编译平台列表生成一个程序集。该程序集包含运行时环境中所需要的所有序列化功能。使用流程如下所示:
6.2.1.点击【Tools -> Odin Inspector -> Preferences -> AOT Generation】按钮来打开Odin AOT自动化功能界面。
6.2.2.点击Odin AOT自动化功能界面中的【Scan Project】按钮来扫描项目工程,此时Odin就会将项目工程中所有的自定义序列化类型全部显示在【Support Serialized Type】下拉列表中。当然,用户也可以在该下拉列表中手动添加序列化类型条目,但是一般不建议这么做。
6.2.3.点击Odin AOT自动化功能界面中的【Generate DLL】按钮来在Assets/Plugins/Sirenix/Assemblies/AOT/路径下生成一个Sirenix.Serialization.AOTGenerated.dll程序集。
6.2.4.当打包构建项目工程结束后,为了避免不同版本Unity无法解析Sirenix.Serialization.AOTGenerated.dll程序集中某些类型而抛出错误提示,建议将Assets/Plugins/Sirenix/Assemblies/AOT/目录删除掉。此时可以采用以下三种删除方式:
6.2.4.1.手动去项目工程中删除该目录。
6.2.4.2.在打包构建脚本中编写代码来删除该目录。
6.2.4.3.在Odin AOT自动化功能界面中首先勾选【Automate Before Builds】按钮;然后勾选【Delete Dll After Builds】按钮来删除该目录。
序列化调试器:用来调试序列化类型中的成员。具有以下特性:
1.查看序列化调试器的源码:首先在Rider当中切换到Assemblies视图。接着在该视图的列表当中选择Sirenix.OdinInspector.Editor程序集。然后从该程序集的列表当中选择Sirenix.Utilities.Editor命名空间。最后从该命名空间的列表当中双击SerializationDebuggerWindow类型,进而可以查看序列化调试器的源码。如下图所示:
2.打开序列化调试器:如果检视面板上不能正常显示指定类型中成员的数值,那么开发者就可以打开序列化调试器来查看该成员的详细序列化信息。目前存在以下两种打开方式:
2.1.在Unity的菜单栏中选择【Tools -> Odin Inspector -> Serialization Debugger】菜单项来打开序列化调试器。如下图所示:
2.2.首先在检视面板中点击组件按钮来弹出下拉菜单。然后从下拉菜单中选择Debug Serialization菜单项来打开序列化调试器。如下图所示:
3.查看类型成员:首先点击类型按钮来弹出类型下拉列表。然后从类型下拉列表中选择一个类型。最后就可以在展示列表上显示选择类型的成员。如下图所示:
4.序列化类型:支持Unity和Odin两种序列化类型。如下图所示:
其中每种序列化类型都含有三种状态。如下所示:
4.1.✔表示序列化成功状态。
4.2.✘表示序列化失败状态。如:不支持的序列化类型或者成员。
4.3.!表示序列化警告状态。如:序列化相关的定制特性使用错误。