Unity JsonUtility的局限性

发表于2018-12-18
评论6 3.45w浏览

最近要做项目关卡编辑的功能,关卡的数据采用Json格式,项目是后期接手的,里面所有数据相关的均采用Json格式 

 

Json序列化和反序列化相关的第三方库非常多,litjson,fastjson,json.net等等,但我依然要采用Unity在5.3以后推出的JsonUtility的原因有两点:

 

1.效率 


2.不依赖第三方库 

 

当前项目中大量的采用json.net进行数据的解析,json.net虽然功能很强大,支持序列化List<T>和Dictionary<TKey,TValue>,并且有方便的自定义特性,但却要付出蛮大的性能代价(Json.Net初始就会分配一个StringBuilder,并未给定大小,很容易引起扩容机制) 

 

实际测试中,我在一款中低端设备上测试json.net解析TimeZone的数据,数据量并不大,大概1000字节,耗时竟然要1-2s,并且堆内存分配以及GC情况都不好,使用Unity自己提供的JsonUtility耗时可以在100s以下,性能差距明显 

 

 

官方在Performance下也有说到 

 

Benchmark tests have shown JsonUtility to be significantly faster than popular .NET JSON solutions (albeit with fewer features than some of them). 

 

JsonUtility比流行的.Net JSON效率高得多(尽管功能上要少一些) 

 

但JsonUtility也是有局限性的: 


1.无法直接序列化和反序列化List<T>和Dictionary<TKey,TValue> 


2.3.0自动属性也无法序列和反序列化 

 

实际上List<T>是支持的,只是无法直接的对List<T>对象进行序列化和反序列化,需要将List<T>包装(Wrap)到一个类当中,然后序列化和反序列化包含这个List<T>的对象即可 

 

Dictionary是确实不支持的,这在官方的文档中也有说明 

 

 

但我们不能因此而放弃它,毕竟效率至上,能省则省,我们要做的就是在JsonUtility局限性或者说是“规则”之下去使用。比如Dictionary可以使用数组来替代,一定要使用Dictionary,则可以进行二次转换。 

 

对于JsonUtility无法序列化和反序列化List<T>和Dictionary<TKey,TValue>的情况,大家如果去搜索相关的资料,应该都会找到下面链接中的解决方案: 

 

 

List<T>,无法直接序列化,将它包装在一个类中就可以了 

 

比如,下面这样是无法序列化的:(我直接使用参考文档中的代码) 


[Serializable] 

public class Enemy 

 

    [SerializeField] 

    string name; 

    [SerializeField] 

    List<string> skills; 

  

    public Enemy(string name, List<string> skills) 

    { 

        this.name = name; 

        this.skills = skills; 

    } 




var enemies = new List<Enemy>(); 

enemies.Add(new Enemy("Json", new List<string>() { "Attack" })); 

enemies.Add(new Enemy("Kate", new List<string>() { "Attack", "Defence" })); 

Debug.Log(JsonUtility.ToJson(enemies));



输出的内容为空,无法序列化enemies到Json 

 

解决方案: 

我们只需要将var enemies = new List<Enemy>();包装到一个类中即可


 

public class EnemyWrap 

 

    public List<Enemy> enemies = new List<Enemy>(); 

  

    public EnemyWrap() 

    {  

        enemies.Add(new Enemy("Json", new List<string>() { "Attack" })); 

        enemies.Add(new Enemy("Kate", new List<string>() { "Attack", "Defence" })); 

    } 

 


放在EnemyWrap类中,再进行序列化操作: 


 

EnemyWrap enemy = new EnemyWrap(); 

Debug.Log(JsonUtility.ToJson(enemy));




输出结果: 



{"enemies":[{"name":"Json","skills":["Attack"]},{"name":"Kate","skills":["Attack","Defence"]}]}

 



下面是它封装好的泛型类List<T>: 


 

// List<T> 

[Serializable] 

public class Serialization<T> 

 

    [SerializeField] 

    List<T> target; 

    public List<T> ToList() { return target; } 

  

    public Serialization(List<T> target) 

    { 

        this.target = target; 

    } 

 



和上面EnemyWrap类是相同的处理方式,只是字段变成了target 

 

JsonUtility不支持Dictionary<TKey,TValue>的序列化和反序列化,上面的网址中提供的方式其实是将Dictionary<TKey,TValue>分成了两个List<T>实现 



// Dictionary<TKey, TValue> 

[Serializable] 

public class Serialization<TKey, TValue> : ISerializationCallbackReceiver 

 

    [SerializeField] 

    List<TKey> keys; 

    [SerializeField] 

    List<TValue> values; 

  

    Dictionary<TKey, TValue> target; 

    public Dictionary<TKey, TValue> ToDictionary() { return 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); 

    } 

 

    public void OnAfterDeserialize() 

    { 

        var count = Math.Min(keys.Count, values.Count); 

        target = new Dictionary<TKey, TValue>(count); 

        for (var i = 0; i < count; ++i) 

        { 

            target.Add(keys[i], values[i]); 

        } 

    } 

 




List<TKey> keys; 

List<TValue> values; 

拆成了两个List,分别存放key和value 

 

这里实现了ISerializationCallbackReceiver,由Unity提供,只有两个接口: 



void OnAfterDeserialize(); 

void OnBeforeSerialize(); 




顾名思义,OnAfterDeserialize在序列化完成之后调用 


OnBeforeSerialize在序列化开始之前调用 

 

OnBeforeSerialize序列化开始之前,将传入进来的Dictionary参数的keys,values存放到 

两个List<T>当中 

 

OnAfterDeserialize序列化完成后,将反序列化回来的两个List,通过for循环的方式还原成 

Dictionary<TKey,TValue> 

 

这样序列出来的结果如下: 



{"keys":[1000,2000],"values":[{"name":"怪物1","skills":["攻击"]},{"name":"怪物2","skills":["攻击","恢复"]}]}  


 

可以看到,里面存放了两个数组keys[…],values[…] 

 

如果你要在实际的项目中,使用上面的Dictionary<Tkey,TValue>,要记得包装(Wrap)一个类,派生自Serialization<TKey, TValue>,这样才会有效 





[Serializable] 

public class SampleDictionary : Serialization<int, int> 

 

    public SampleDictionary(Dictionary<int, int> targe) : base(targe) { } 




对于作者提供的Dictionary<TKey,TValue>方案,需要我们在过程中注意Dictionary的实现是由两个List<T>组成的。 

 

通常来讲,我们使用Dictonary<TKey,TValue>主要是哈希的查找效率,如果不是对大量数据进行频繁的查找,那么可以使用数组来替代Dictionary<TKey,TValue>,或是你的数据结构比较简单, 

可以以字符串的形式,通过特定的标志,在OnAfterDeserialize接口, 将字符串分割再转换成Dictionary<Tkey,TValue>等等 

 

目前使用JsonUtility来处理Dictionary<Tkey,TValue>是要二次转换的,总之要在“规则”之下使 ,具体项目中也要根据需求来决定是否采用。 

 

 

此外,Unity的JsonUtility也提供了很多其它实用的功能,比如支持序列化和反序列化MonoBehaviour和ScriptableObject的派生类,我们可以将他们序列化成数据流,加密,保存为本地文件,或是通过网络进行传输,但解析需要使用FromJsonOverwrite, Unity实现的一个特定的重写版本 

 

EditorJsonUtility还可以对所有的UnityEngine.Object进行序列化和反序列化操作,在做一些辅助编辑功能的时候会有用

 

下面也把JsonUtility的基本使用也贴上来,比较啰嗦(记忆力不好木办法),也方便以后回来查看 

 

JsonUtility反序列化需要我们根据Json数据的格式生成对应的Struct或是Class: 
{}-表示类或结构体 

[]-表示数组 

"":xxx-表示字段 

 

比如下面的Json数据结构: 



 

    "viewTypeList": [{ 

            "Type": "SelectKitchenTypeView", 

            "Path": "xxx/xxx" 

        }, 

        { 

            "Type": "SelectKitchenTypeView", 

            "Path": "xxx/xxx" 

        }, 

        { 

            "Type": "SelectKitchenTypeView", 

            "Path": "xxx/xxx" 

        } 

    ] 



首先最上层是一个大{},我们需要定义一个类,可以是任意的名字,比如叫ViewTypeData 

下一层是viewTypeList,是viewTypeList:xxx格式,说明是一个字段,后面是[],说明这个字段是一个数组,我们需要在ViewTypeData中定义以viewTypeList命名的数组,继续向下看: 

 


 

   "Type": "SelectKitchenTypeView", 

   "Path": "xxx/xxx" 

}, 



 

是一个类或结构体,我们需要定义它,并定义Type和Path两个string字段,它就是我们上面数组的类型,最后Json数据的反序列化结构如下: 

 


[Serializable] 

    public class ViewTypeInfo 

    { 

        public string Type; 

        public string Path; 

     } 

  

    [Serializable] 

    public class ViewTypeData 

    { 

        public ViewTypeInfo[] viewTypeList; 

    } 

 

sample:  

TextAsset text = Resources.Load<TextAsset>(path); 

ViewTypeData ret= JsonUtility.FromJson<ViewTypeData>(text.text);




如果上面的Json结构,变成如下这样: 




 

    "data": { 

        "viewTypeList": [{ 

                "Type": "SelectKitchenTypeView", 

                "Path": "xxx/xxx" 

            }, 

            { 

                "Type": "SelectKitchenTypeView", 

                "Path": "xxx/xxx" 

            }, 

            { 

                "Type": "SelectKitchenTypeView", 

                "Path": "xxx/xxx" 

            } 

        ] 

    } 

 



 

外面又套了一层data,那么就需要再包装一个类来保存ViewTypeData对象, 

如下: 



public class TypeData 

    { 

   public ViewTypeJson data; 

    } 

 

Sample: 

TextAsset text = Resources.Load<TextAsset>(path); 

TypeData ret = JsonUtility.FromJson<TypeData> (text.text); 



 

需要注意的是,Json字段和类定义字段的名字要一一对应,比如上面的data,viewTypeList,Type,Path都是要保持类中定义和Json字段是一样的, 

写错并不会报错,只是无法反序列化 

 

关于JsonUtility堆内存分配问题: 

 

GC Memory usage is at a minimum:  

  

ToJson() allocates GC memory only for the returned string.  

  

FromJson() allocates GC memory only for the returned object, as well as any subobjects needed (e.g. if you deserialize an object that contains an array, then GC memory will be allocated for the array).  

  

FromJsonOverwrite() allocates GC memory only as necessary for written fields (for example strings and arrays). If all fields being overwritten by the JSON are value-typed, it should not allocate any GC memory.  

  

Using the JsonUtility API from a background thread is permitted. As with any multithreaded code, you should be careful not to access or alter an object on one thread while it is being serialized/deserialized on another. 

 

这是官方文档中的一段话,没什么特别的,引用类型会分配堆内存,值类型不会,并且JsonUtility也可以在多线程中使用,但要注意多线程之间的同步问题

 

对于不参与序列化或反序化的字段,可以添加特性[NonSerialized] 

像这种字段,通常是需要由其它的字段计算而来的,比如面积,周长等等 

 

 

这里有网友做了一个便捷的转换工具,直接将Json数据粘贴进去,点击生成就可以得到实体类的结构 


 

 

参考:

 

 

在线Json工具推荐: 


 

感谢您的阅读,如文中有误,欢迎指正,共同提高


欢迎关注我个人技术分享的微信公众号,Paddtoning帕丁顿熊,期待和您的交流






如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引