Unity的數(shù)據(jù)存儲(chǔ),本地類 PlayerPrefs, Inspector,以及Prefab等都使用了序列化與反序列化的知識.
循序漸進(jìn),讓我們一步步了解Unity中的序列化和反序列化的知識;
流與格式化器
序列化: 將對象轉(zhuǎn)換為字節(jié)流.
反序列化: 將字節(jié)流轉(zhuǎn)換為對象.
直接講概念太抽象,我們先來看代碼;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class Test : MonoBehaviour
{
private void Start()
{
Hero hero_ins = new Hero();
hero_ins.id = 100;
hero_ins.attack = 99f;
hero_ins.defence = 99f;
hero_ins.name = "Calabash";
Stream st = FormatInstanceToMemory(hero_ins);
st.Position = 0;
hero_ins = null;
hero_ins = MemoryToInstance(st) as Hero;
Debug.Log(hero_ins.id.ToString());
Debug.Log(hero_ins.attack.ToString());
Debug.Log(hero_ins.defence.ToString());
Debug.Log(hero_ins.name);
}
//序列化 把實(shí)例對象寫入流中
private static MemoryStream FormatInstanceToMemory(object instance)
{
//創(chuàng)建一個(gè)流
MemoryStream ms = new MemoryStream();
//創(chuàng)建格式化器
BinaryFormatter bf = new BinaryFormatter();
//序列化為二進(jìn)制流
bf.Serialize(ms, instance);
return ms;
}
//反序列化, 從流中讀出實(shí)例對象
private static object MemoryToInstance(Stream st)
{
//創(chuàng)建格式化器
BinaryFormatter bf = new BinaryFormatter();
//把二進(jìn)制流反序列化為指定的對象
return bf.Deserialize(st);
}
}
關(guān)于Hero類的定義如下:
[Serializable] //注意這個(gè)關(guān)鍵字
public class Hero
{
public int id;
public float attack;
public float defence;
public string name;
}
代碼中的注釋已經(jīng)寫得很清楚了,通過代碼我們要解釋三個(gè)概念;
- 流(Stream): Unity中的二進(jìn)制數(shù)據(jù)流,有 MemoryStream, FileStream 等子類來處理不同場景的數(shù)據(jù)流,但我們這里不討論每種流的用法,只需要讓大家理解 Stream提供了一個(gè)用來容納經(jīng)過序列化之后的字節(jié)塊的容器
更多的Stream知識可以查閱這里: Unity的Stream流 - 格式化器: 使用序列化和反序列的工具,代碼中只是使用到了 BinaryFormatter 這種格式化器,其實(shí)還有 SoapFormatter (需要導(dǎo)入對應(yīng)的.dll文件),需要注意的是進(jìn)行序列化和反序列的操作必須是相同的格式化器,否則可能會(huì)拋出System.Runtime.Serialization.SerializationException異常.
- [Serializable]特性: 默認(rèn)自定義的類型是無法被序列化的,需要使用 [Serializable] 特性來實(shí)現(xiàn)序列化與反序列化,關(guān)于此特性更多的內(nèi)容見下節(jié);
通過上面的示例,我們往流中寫入了一個(gè)對象,那么可以寫入兩個(gè),甚至多個(gè)不同的對象么?答案是肯定的,我們還是用代碼測試一下;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class Test : MonoBehaviour
{
private void Start()
{
Hero hero_ins = new Hero();
hero_ins.id = 100;
hero_ins.attack = 99f;
hero_ins.defence = 99f;
hero_ins.name = "Calabash";
Soldier soldier_ins = new Soldier();
soldier_ins.life = 50;
soldier_ins.weapon = "hammer";
//創(chuàng)建一個(gè)流
MemoryStream ms = new MemoryStream();
//創(chuàng)建格式化器
BinaryFormatter bf = new BinaryFormatter();
//序列化為二進(jìn)制流
bf.Serialize(ms, hero_ins);
bf.Serialize(ms, soldier_ins);
ms.Position = 0;
hero_ins = null;
soldier_ins = null;
//從數(shù)據(jù)流中讀出數(shù)據(jù)
//讀出的順序不能顛倒,因?yàn)槭菑膍s的開端讀取,因此要按寫入的順序讀取
hero_ins = bf.Deserialize(ms) as Hero;
soldier_ins = bf.Deserialize(ms) as Soldier;
Debug.Log("hero: " + hero_ins.id.ToString());
Debug.Log("hero: " + hero_ins.attack.ToString());
Debug.Log("hero: " + hero_ins.defence.ToString());
Debug.Log("hero: " + hero_ins.name);
Debug.Log("soldier: " + soldier_ins.life.ToString());
Debug.Log("soldier: " + soldier_ins.weapon);
}
[Serializable]與[NonSerialized]的繼承
1. [Serializable]
該特性只能用于以下類型:
- 引用類型(class)
- 值類型(struct)
- 枚舉類型(enum)
- 委托類型(delegate)
該特性不會(huì)被派生的子類繼承;
[Serializable] //注意這個(gè)關(guān)鍵字
public class Hero
{
public int id;
public float attack;
public float defence;
public string name;
}
public class GirlHero : Hero
{
public int girlAge;
}
public class Test : MonoBehaviour
{
private void Start()
{
Hero hero_ins = new Hero();
hero_ins.id = 100;
hero_ins.attack = 99f;
hero_ins.defence = 99f;
hero_ins.name = "Calabash";
GirlHero girl_ins = new GirlHero();
girl_ins.girlAge = 18;
//創(chuàng)建一個(gè)流
MemoryStream ms = new MemoryStream();
//創(chuàng)建格式化器
BinaryFormatter bf = new BinaryFormatter();
//序列化為二進(jìn)制流
bf.Serialize(ms, hero_ins);
bf.Serialize(ms, girl_ins);
ms.Position = 0;
hero_ins = null;
girl_ins = null;
//從數(shù)據(jù)流中讀出數(shù)據(jù)
//讀出的順序不能顛倒,因?yàn)槭菑膍s的開端讀取,因此要按寫入的順序讀取
hero_ins = bf.Deserialize(ms) as Hero;
girl_ins = bf.Deserialize(ms) as GirlHero;
Debug.Log("hero: " + hero_ins.id.ToString());
Debug.Log("hero: " + hero_ins.attack.ToString());
Debug.Log("hero: " + hero_ins.defence.ToString());
Debug.Log("hero: " + hero_ins.name);
Debug.Log("girl: " + girl_ins.girlAge.ToString());
}
}
點(diǎn)擊運(yùn)行后,果不其然會(huì)報(bào) SerializationException 的一個(gè)錯(cuò)誤:
SerializationException: Type 'GirlHero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
錯(cuò)誤信息很明顯,我們的 GirlHero 類沒有標(biāo)記 Serializable 特性,當(dāng)我們給這個(gè)類也標(biāo)記上該特性后,結(jié)果可以正常被打印;
我們在來看另外一種情況,只有派生類使用特性,基類不使用:
public class Hero
{
public int id;
public float attack;
public float defence;
public string name;
}
[Serializable]
public class GirlHero : Hero
{
public int girlAge;
}
運(yùn)行后報(bào)錯(cuò)如下:
SerializationException: Type 'Hero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
通過這個(gè)測試,我們可以知道:
- Serializable特性不會(huì)被繼承,每個(gè)派生類如果要想被序列化,需要單獨(dú)添加此特性字段.
-
派生類添加了 Serializable 特性,而基類不使用,那么從基類派生的任何類都無法被序列化.
可以這么理解,基類如果無法被序列化,那么它的字段無法被序列化,派生類同樣包含該基類的字段,那么自然也是無法被序列化的.C#中的所有類都是繼承自 System.Object 類,這個(gè)類已經(jīng)應(yīng)用了 Serializable 特性.
2. [NonSerialized]
在默認(rèn)情況下,序列化會(huì)讀取對象的所有字段,無論這些字段生命的訪問權(quán)限是 public 還是 private, 如果我們有些敏感字段或者計(jì)算屬性不想被序列化,有沒有辦法呢?
在不想被序列化的字段上面使用 NonSerialized 屬性即可;
[Serializable] //注意這個(gè)關(guān)鍵字
public class Hero
{
public int id;
[NonSerialized]
public float attack;
public float defence;
public string name;
}
使用上面Test的腳本,打印結(jié)果如下:
我們可以看到反序列化后,被標(biāo)記為 NonSerialized 特性的字段值變?yōu)榱?,這是由于
attack 字段不能被序列化,它的值99并不會(huì)寫入到流中,因此被反序列化后,其余字段都能夠被正常賦值,該字段由于從流中讀取不到對應(yīng)的值,只能設(shè)置為0;
那么能不能在反序列化的時(shí)候,把正確的值賦值回去呢?答案是肯定的,我們下節(jié)再來解決這個(gè)問題,我們繼續(xù)查看 NonSerialized 的繼承特點(diǎn);
[Serializable] //注意這個(gè)關(guān)鍵字
public class Hero
{
public int id;
[NonSerialized]
public float attack;
public float defence;
public string name;
}
[Serializable]
public class GirlHero : Hero
{
public int girlAge;
}
public class Test : MonoBehaviour
{
private void Start()
{
GirlHero girl_ins = new GirlHero();
girl_ins.id = 100;
girl_ins.attack = 99f;
girl_ins.defence = 99f;
girl_ins.name = "Calabash";
girl_ins.girlAge = 18;
//創(chuàng)建一個(gè)流
MemoryStream ms = new MemoryStream();
//創(chuàng)建格式化器
BinaryFormatter bf = new BinaryFormatter();
//序列化為二進(jìn)制流
bf.Serialize(ms, girl_ins);
ms.Position = 0;
girl_ins = null;
//從數(shù)據(jù)流中讀出數(shù)據(jù)
girl_ins = bf.Deserialize(ms) as GirlHero;
Debug.Log("girl_id: " + girl_ins.id.ToString());
Debug.Log("girl_attack: " + girl_ins.attack.ToString());
Debug.Log("girl_defence: " + girl_ins.defence.ToString());
Debug.Log("girl_name: " + girl_ins.name);
Debug.Log("girl_age: " + girl_ins.girlAge.ToString());
}
}
打印結(jié)果如下:
通過上面的測試可以得知: [NonSerialized] 特性可以被派生類繼承;
控制序列化和反序列化的流程
在上一節(jié)提出的問題,對于 NonSerialized 修飾的字段,在反序列化的時(shí)候應(yīng)該如何賦值,以及如果我們想在序列化和反序列化之前和之后做些操作,應(yīng)該怎么實(shí)現(xiàn)?
[Serializable]
public class GirlHero : Hero
{
public int girlAge;
[OnDeserialized]
private void CaculateAttack(StreamingContext context)
{
this.attack = 1000;
}
}
在上一節(jié)的代碼基礎(chǔ)上,我們對 GirlHero 做了上面的改動(dòng),增加了一個(gè) CaculateAttack 方法,并且使用了 [OnDeserialized] 特性,我們再來看打印結(jié)果:
通過這樣的方法和特性我們對 attack 字段在反序列化的時(shí)候進(jìn)行了賦值;
從特性的名字可以看出,是在反序列化過程完成后調(diào)用所修飾的方法,還有其他三個(gè)相關(guān)特性我們一起來看看;
1. 序列化與反序列化過程的方法特性
- OnSerializing :格式化器在序列化對象字段之前,調(diào)用該特性修飾的方法.
- OnSerialized :格式化器在序列化對象字段之后,調(diào)用該特性修飾的方法.
- OnDeserializing :格式化器在反序列化對象字段之前,調(diào)用該特性修飾的方法.
- OnDeserialized ::格式化器在反序列化對象字段之后,調(diào)用該特性修飾的方法.
這幾個(gè)特性是在 System.Runtime.Serialization 命名空間下,共同點(diǎn)是用來修飾類型中定義的方法;注意他們的調(diào)用時(shí)機(jī).
2. StreamingContext
在上面的實(shí)例代碼中,可以看到方法參數(shù)是一個(gè) StreamingContext 類,這個(gè)類是序列化與反序列化時(shí)流的上下文,我們通過程序集可以看到,這個(gè)類型是一個(gè)值類型.
public struct StreamingContext
{
//調(diào)用方定義的附加上下文引用,一般為空
public object Context {
get;
}
//用來標(biāo)記序列化和反序列對象的來源和目的地
public StreamingContextStates State {
get;
}
//構(gòu)造方法
public StreamingContext (StreamingContextStates state);
public StreamingContext (StreamingContextStates state, object additional);
//重載System.Object方法
public override bool Equals (object obj);
public override int GetHashCode ();
}
通過State的屬性我們可以查看序列化和反序列化時(shí)對應(yīng)的來源和目的地,更多的信息請查閱這里:StreamingContextStates枚舉
我們在上面序列化時(shí)使用的格式化器的 Context 屬性就是 StreamingContext, 它的 State 屬性默認(rèn)是All,我們也可以在創(chuàng)建格式化器的時(shí)候手動(dòng)指定 State 的類型來滿足不同的需求,比如:
//指定state類型,深度克隆一個(gè)對象
BinaryFormatter bf = new BinaryFormatter();
bf.Context = new StreamingContext(StreamingContextStates.Clone);
Unity的Inspector
在屬性監(jiān)視板中可以看到游戲腳本中某個(gè)對象的信息,這些字段和值并不是Unity調(diào)用游戲腳本中的C#接口獲取的,而是通過顯示對象的反序列化得到這些屬性數(shù)值,然后在面板中展示出來;
Unity的Prefab
Prefab是Unity中很重要的一種資源類型,真正實(shí)現(xiàn)了游戲?qū)ο蟮目寺?預(yù)制體是游戲?qū)ο蠛徒M件經(jīng)過序列化后得到的文件,它的格式可以是二進(jìn)制的也可以是文本文件,可以通過下面的選項(xiàng)來設(shè)置:
它的特點(diǎn)如下:
- 可以被放入多個(gè)場景中,也可以在一個(gè)場景中放入多個(gè)
- 在場景中增加一個(gè)Prefab,就實(shí)例化了一個(gè)該P(yáng)refab的實(shí)例
- 所有的Prefab實(shí)例都是Prefab的克隆,因此在運(yùn)行中生成Prefab實(shí)例的話可以看到這些實(shí)例會(huì)帶有(Clone)的標(biāo)記
- 只要Prefab的原型發(fā)生了變化,場景中所有的prefab實(shí)例都會(huì)發(fā)生變化
腳本創(chuàng)建Prefab實(shí)例我們都是通過Instantiate方法:
public static Object Instantiate (Object original, Vector3 position, Quaternion rotation)
在該方法內(nèi)部,會(huì)首先將參數(shù)original所引用的游戲?qū)ο笮蛄谢?得到序列化流后,再使用反序列化機(jī)制將這個(gè)序列化流生成一個(gè)新的游戲?qū)ο?可以說是對象的克隆操作;
Unity在System.Runtime.Serialization命名空間下定義了一個(gè)FormatterServices的類型,只包含一些靜態(tài)方法,用來輔助序列化與反序列化的過程;
序列化過程
- 調(diào)用FormatterServices的 GetSerializableMembers ;
//兩個(gè)重載版本
//type: 正在序列化或克隆的類型
//context: 發(fā)生序列化的上下文
//MemberInfo[]: 返回類型對象的數(shù)組,每一個(gè)元素都對應(yīng)一個(gè)可以成員字段的名稱
public static MemberInfo[] GetSerializableMembers(Type type, StreamingContext context)
public static MemberInfo[] GetSerializableMembers(Type type)
- 調(diào)用FormatterServices的 GetObjectData ;
//obj: 表示要寫入序列化程序的對象實(shí)例
//members: 代表的是第一步提取的成員字段的名稱
//Object[]: 返回的是對應(yīng)members中每個(gè)元素表示的字段對應(yīng)的值,理解為Value的集合
public static Object[] GetObjectData(Object obj, MemberInfo[] members)
- 經(jīng)過前兩個(gè)步驟獲取了對象的成員和其對應(yīng)的值,這一步先把程序集標(biāo)識以及類型的完整名稱寫入流中.
- 格式化器遍歷第一步與第二步得到的數(shù)組獲取成員名稱和其對應(yīng)的值,將這些信息寫入流中.
反序列化過程
- 格式化器從流中讀取程序集標(biāo)識和完整的類型名稱,然后調(diào)用FormatterServices的 GetTypeFromAssembly ;
//assem: 讀取到的程序集標(biāo)識
//name: 完整的類型名稱
//Type: 返回值便是反序列化對象的實(shí)際類型
public static Type GetTypeFromAssembly(Assembly assem, string name)
- 獲取了對象的類型后,接下來就是要在為新的對象分配一塊內(nèi)存空間,調(diào)用FormatterServices的 GetUninitializedObject ;
//為指定類型分配內(nèi)存空間
public static Object GetUninitializedObject(Type type)
需要注意的是,此時(shí)還沒有調(diào)用構(gòu)造函數(shù),對象的所有字節(jié)都被初始化為 null 或者 0 ;
- 分配好內(nèi)存空間后,還是調(diào)用FormatterServices的 GetSerializableMembers 構(gòu)造并初始化一個(gè)新的 MemberInfo 數(shù)組;
這個(gè)方法的說明見序列化過程的第一步;
調(diào)用方法后獲取該類型的所有成員字段名稱的集合 MemberInfo[] members ; - 獲取到字段信息后,這一步就要獲取字段對應(yīng)數(shù)組的信息;格式化器會(huì)根據(jù)流中包含的數(shù)據(jù)創(chuàng)建一個(gè) Object 數(shù)組,對其進(jìn)行初始化;
到了這一步,你就有了一個(gè)未初始化的對象,一個(gè)成員變量集合和對應(yīng)數(shù)值的集合; - 這一步就要調(diào)用FormatterServices的 PopulateObjectMembers 方法對實(shí)例對象初始化;
//obj: 表示剛才創(chuàng)建要被初始化的對象實(shí)例
//members: 對象需要被填充的成員或者屬性
//data: 對象需要被填充的成員或者屬性對應(yīng)的數(shù)值
//Object: 返回一個(gè)初始化好的實(shí)例對象
public static Object PopulateObjectMembers(Object obj, MemberInfo[] members, Object[] data)
參考文章: <<Unity3D腳本編程>> 陳嘉棟