研究序列化和反序列化时遇到一些困惑,所以做一些实验,记录一下。
写一个测试代码:
using UnityEngine;
using System;
// 注意到,MonoBehaviour的子类不需要[Serializable]属性,它的公共成员也可以在Inspector中显示
public class MyMonoBehaviour : MonoBehaviour, ISerializationCallbackReceiver
{
[Serializable]
public class MySerializableClass
{
public int myInt;
void OnValidate()
{
Debug.Log("MySerializableClass OnValidate");
}
}
[Serializable]
public class MySerializationCallbackReceiverClass : ISerializationCallbackReceiver
{
public int myInt;
void OnValidate()
{
Debug.Log("MySerializationCallbackReceiverClass OnValidate");
}
public void OnBeforeSerialize()
{
Debug.Log("MySerializationCallbackReceiverClass OnBeforeSerialize");
}
public void OnAfterDeserialize()
{
Debug.Log("MySerializationCallbackReceiverClass OnAfterDeserialize");
}
}
public class MyFieldMonoBehaviour : MonoBehaviour
{
public int myInt;
void OnValidate()
{
Debug.Log("MyFieldMonoBehaviour OnValidate");
}
}
public int myInt;
public MySerializableClass mySerializableClass;
public MySerializationCallbackReceiverClass mySerializationCallbackReceiverClass;
public MyFieldMonoBehaviour myFieldMonoBehaviour;
void OnEnable()
{
Debug.Log("MyMonoBehaviour OnEnable");
}
void OnDisable()
{
Debug.Log("MyMonoBehaviour OnDisable");
}
void OnValidate()
{
Debug.Log("MyMonoBehaviour OnValidate");
}
public void OnBeforeSerialize()
{
Debug.Log("MyMonoBehaviour OnBeforeSerialize");
}
public void OnAfterDeserialize()
{
Debug.Log("MyMonoBehaviour OnAfterDeserialize");
}
}在 Inspector 中显示:

注意到,打开 Inspector 时,OnBeforeSerialize 持续地被调用,而在关闭界面后,被调用就停止了。
由于 OnBeforeSerialze 一直在刷屏,我们把它们的 Debug Log 注释化。编译完成后得到:
注意到我们已经注释化了Debug Log,为什么还是打印了信息?推测是 Unity Editor在导入新的脚本之前,先用老的脚本序列化受影响的组件,再用新的脚本反序列化组件。
这里也可以看出,OnBeforeSerialize 是先 this 后字段,而 OnAfterDeserialze 是先字段后 this。
不知道为什么调用了 MyMonoBehaviour 的 OnValidate,但没有调用字段的。
现在,打开 Inspector(没有出现新的 Log):
修改第一个 My Int 为 1,出现新的 Log:
修改第二个 My Int 为 1,出现新的 Log:

修改第三个 My Int 为 1,出现新的 Log:

由此看来,无论修改了哪个字段,Unity Editor 进行的操作都是统一的,即反序列化整个MonoBehaviour。
在刚才的实验里,与其他字段默认被无参构造不同,作为字段的 MonoBehaviour 总是被反序列化为 null。这可能是因为 MonoBehaviour 子类的字段默认有类似 [SerializeReference] 的效果。我们加入 [SerializeReference] 的字段来尝试:
[SerializeReference]
public int myIntReference;
[SerializeReference]
public MyStruct myStructReference;
[SerializeReference]
public MySerializationCallbackReceiverClass mySerializationCallbackReceiverClassReference;噢,编译器报错了:
The field MyMonoBehaviour.myStructReference has the [SerializeReference] attribute applied, but is of type MyMonoBehaviour.MyStruct, which is a value type. By definition, value types cannot be serialized by reference.
int 并没有像 struct 那样导致报错,但是按照它的说法“value types cannot be serialized by reference”,给 int 加 [SerializeReference] 应该是没有作用的。我们把 int 和 struct 删掉,得到:

什么都没有显示,推测是因为已经被反序列化为 null 了。
尝试了给 MonoBehaviour 子类的字段加上 [SerializeField],并没有变化,推测它的行为不受 [SerializeField] 和 [SerializeReference] 影响。
如果这个 MonoBehaviour 子类的字段不是 null 呢?
void Awake()
{
myFieldMonoBehaviour = gameObject.AddComponent<MyFieldMonoBehaviour>();
}
修改 My Field Mono Behaviour 中的 My Int 为 1:

修改 My Mono Behaviour 中的第一行 My Int 为2:

可以看出,两个 MonoBehaviour 互不影响。
参考了 Unity 官方文档,暂时得出结论:
1. 在 Inspector 中显示组件时,会持续地对其序列化,无论是否在播放游戏。
2. C# 值类型和字典只能被内联序列化。MonoBehaviour 的子类只能被 [SerializeReference] 序列化,不受 attribute 影响。
3. 在 Inspector 中显示组件时,会递归地显示内联序列化的字段(可以展开或折叠),而 [SerializeReference] 序列化的字段只会显示引用,无法展开。
4. 在 Inspector 中修改组件时,无论是修改组件的字段,还是修改内联递归展开的字段的字段,都会导致整个组件反序列化。组件反序列化时,会递归地反序列化所有内联序列化的字段,调用它们的 OnAfterDeserialize(即使它们的内存空间可能根本就没有被修改),然后自身 OnAfterDeserialize。
5. 在 Inspector 中修改组件导致的反序列化的发生时机是:文本框中输入新的字符时。选择和取消选择文本框不会导致反序列化。
暂时研究到这里。这对我写代码的指导是:
1. 不要往 OnBeforeSerialize 里面塞任何大的操作,最好就不要塞什么实质性操作,否则打开 Inspector 就要爆炸。
2. OnAfterDeserialize 也避免放过大的操作。
Unity 官网有一个关于自定义序列化的例子:
https://docs.unity3d.com/cn/current/Manual/script-serialization-custom-serialization.html
这个例子里,MonoBehaviour 里面装了一棵树。OnBeforeSerialize 时,它就递归遍历这棵树的所有节点,把它们全部装进一个 List(List是可以被内联序列化的),而 OnAfterDeserialize 时,它就把它们从 List 中拿出来,重新组装成一棵树。
这看起来很优雅,但我现在要质疑,这真的好吗?因为在它的代码里,每次OnBeforeSerialize 都会先将 List 清空,然后重新遍历整棵树,将节点全装进去。这是一个MonoBehaviour,所以我们可以想象打开 Inspector 查看它,此时 OnBeforeSerialize 会被持续不断地调用……我不知道会怎么样,但我猜如果树的数据规模大一些的话,电脑可能要耗不必要地开始卡了。理论上说官方文档给的例子应该是比较值得信任的,但现在我真的要质疑一下。
事实上如果这个类不是 MonoBehaviour 的子类,也不是其它组件的字段(它可能是某个非组件的单例的字段),那么你就不会在 Inspector 中发现它,这样的话其实就也没问题。或者你能忍住不手欠查看这个组件,或者能接受电脑变卡,那也没问题……
感觉还是没有彻底搞清楚。不知道有没有评论区大佬能讲明白,或者等我以后自己搞明白了再来补充这篇文章吧。
--------2026.3.2更新--------
又发现一个特性:[Serializable] 类的引用类型字段如果没有 [SerializeReference],似乎无法以null 初始化。如果默认参数填 null,编译器不会报错,但 Unity 会以一种不明的方式进行初始化,疑似是递归调用无参构造函数……


暂无关于此日志的评论。