Unity JsonUtility 序列化链表和字典

前言

为什么要用 JsonUtility 呢?

  1. 第三方的 Json 库很多是不适配最新版 Unity 的, 另外就是对于强迫症而言, 难道你不想用 Unity 官方的吗, 难道不想自动更新吗? 一直需要手动更新也是很麻烦的.

  2. 一般来说第三方的 Json 库都是不支持序列化 MonoBehaviour 类和 ScriptableObject 类的.

  3. 据 Unity 手册上描述, JsonUtility 比目前流行的 .NET JSON 解决方案要快得多, 缺点就是 JsonUtility 提供的功能很少. 下面是 Unity 手册的原文:

垃圾收集 (GC) 内存使用量为最低量:

  • ToJson 仅为返回的字符串分配 GC 内存.
  • FromJson 仅为返回的对象以及所需的所有子对象分配 GC 内存 (例如: 如果对包含数组的对象进行反序列>化,则 Unity 将为该数组分配 GC 内存)
  • FromJsonOverwrite 仅根据需要为写入的字段 (例如字符串和数组)分配 GC 内存. 这意味着, 如果 JSON >覆盖的所有字段都是值类型, 则 Unity 不会分配任何 GC 内存.
  • 可以使用后台线程中的 JsonUtility API. 但是, 与任何多线程代码一样, 在一个线程上序列化或反序列化对>象时, 请勿在另一个线程上访问或更改该对象.

Unity 手册: Json 序列化

JsonUtility 使用条件

  1. JsonUtility 支持任何 MonoBehaviour 子类, ScriptableObject 子类或者带有 [Serializable] 属性的普通类或结构. 但是, 将 JSON 反序列化为 MonoBehaviourScriptableObject 子类时,必须使用 FromJsonOverwrite 方法, 如果尝试使用 FromJson 则 Unity 会抛出异常.

  2. 将对象传入到标准 Unity 序列化程序进行处理时, 需要遵循与在 Inspector 中相同的规则和限制, 比如: Unity 只序列化字段, 不序列化属性;

  3. 另外此 API 不支持类似 Dictionary<> 的类型; 也不支持将其他类型直接传递到 API, 例如原始类型或数组. 如果需要转换上述类型, 则需要将它们包裹在某种 class 或 struct 中.

API 链接

https://docs.unity3d.com/cn/2020.2/ScriptReference/JsonUtility.html

另外还有一个编辑器模式下专用的 API: EditorJsonUtility

它的链接是: https://docs.unity3d.com/cn/2020.2/ScriptReference/EditorJsonUtility.html

如何使用 JsonUtility 序列化数组 [] 和链表 List<>

由于 JsonUtility 不支持直接序列化数组和链表, 因此需要首先将其包装在一个 class 或者 struct 中, 之后对 class 或者 struct 进行序列化.

以 List<> 和 class 为例. 使用特性 [Serializable] 修饰 class, 使 class 成为可序列化类型, 最后使用 [SerializeField] 修饰 List<>, 这样链表就可以被序列化成 Json 文本了.

1
2
3
4
5
6
7
8
9
[Serializable]
public class Enemy
{
[SerializeField]
public int id;

[SerializeField]
public List<string> skills;
}

上面的字符串链表中的元素类型是 string, 是可以序列化的, 另一个成员 int 类型, 也可以序列化, 只要将类修饰为 [Serializable], 字段修饰为 [SerializeField] 便可以使用 JsonUtility 进行序列化了.

注: 严格来说, 如果基础类型使用的是 public 修饰, 那么就不必使用 [SerializeField] 进行修饰, 如上面的 id 成员, 但是链表不是基础类型, 即使使用 public 修饰, 也必须使用 [SerializeField] 进行修饰.

1
2
3
4
5
6
7
8
[Serializable]
public class Enemy
{
public int id;

[SerializeField]
public List<string> skills;
}

但是如果 id 是 private 类型的, 那么还是需要使用 [SerializeField] 进行修饰. 基础类型的序列化规则和 Inspector 面板的序列化规则是相同的.

如何使用 JsonUtility 序列化字典 Dictionary<>

字典即使使用上述方式也是无法进行序列化的, 这里需要使用到 Unity 提供的 ISerializationCallbackReceiver 接口.

这个接口要求实现两个方法: OnBeforeSerialize()OnAfterDeserialize().

public void OnBeforeSerialize()

这个方法会在 序列化之前 调用.

public void OnAfterDeserialize()

这个方法会在 反序列化之后 调用.

我们的思路是: 将字典放到一个新的 class 中, 由于字典不能序列化, 但是链表可以通过使用 [SerializeField] 修饰来进行序列化, 因此可以使用两个 List<> 分别保存所有的键和所有的值, 并使用 [SerializeField] 进行修饰, 这样只要在序列化之前将字典中的值放到链表中即可, 最后序列化出来的结果就是两个链表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[SerializeField]
private List<TKey> keys;

[SerializeField]
private List<TValue> values;

Dictionary<TKey, TValue> target;

public Serialization(Dictionary<TKey, TValue> target)
{
this.target = target;
}

public void OnBeforeSerialize()
{
keys = new List<TKey>(target.Keys);
values = new List<TValue>(target.Values);
}

由于序列化后是两个链表, 那么反序列化出来的数据也是两个链表, 此时就需要将这两个链表转换为字典.

1
2
3
4
5
6
7
8
9
10
11
12
public void OnAfterDeserialize()
{
if (keys.Count == values.Count)
{
target = new Dictionary<TKey, TValue>(count);

for (var index = 0; index < count; ++index)
{
target.Add(keys[index], values[index]);
}
}
}

最后再补一个将反序列化的字典返回的方法.

1
2
3
4
public Dictionary<TKey, TValue> ToDictionary()
{
return target;
}

使用方法:

1
2
3
4
5
6
// 序列化
var data = new Serialization<int, Enemy>(enemies);
string json = JsonUtility.ToJson(data);

// 反序列化
Dictionary<int, Enemy> enemies = JsonUtility.FromJson<Serialization<int, Enemy>>(json).ToDictionary();

参考链接

【Unity】JsonUtility で List と Dictionary<TKey,TValue> シリアライズする