Unity中的序列化與反序列化

Unity的數(shù)據(jù)存儲,本地類 PlayerPrefs, Inspector,以及Prefab等都使用了序列化與反序列化的知識.
循序漸進,讓我們一步步了解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);
    }

    //序列化 把實例對象寫入流中
    private static MemoryStream FormatInstanceToMemory(object instance)
    {
        //創(chuàng)建一個流
        MemoryStream ms = new MemoryStream();
        //創(chuàng)建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化為二進制流
        bf.Serialize(ms, instance);

        return ms;
    }
    //反序列化, 從流中讀出實例對象
    private static object MemoryToInstance(Stream st)
    {
        //創(chuàng)建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //把二進制流反序列化為指定的對象
        return bf.Deserialize(st);
    }

}

關于Hero類的定義如下:

[Serializable] //注意這個關鍵字
public class Hero
{
    public int id;
    public float attack;
    public float defence;
    public string name;
}

代碼中的注釋已經(jīng)寫得很清楚了,通過代碼我們要解釋三個概念;

  • 流(Stream): Unity中的二進制數(shù)據(jù)流,有 MemoryStream, FileStream 等子類來處理不同場景的數(shù)據(jù)流,但我們這里不討論每種流的用法,只需要讓大家理解 Stream提供了一個用來容納經(jīng)過序列化之后的字節(jié)塊的容器
    更多的Stream知識可以查閱這里: Unity的Stream流
  • 格式化器: 使用序列化和反序列的工具,代碼中只是使用到了 BinaryFormatter 這種格式化器,其實還有 SoapFormatter (需要導入對應的.dll文件),需要注意的是進行序列化和反序列的操作必須是相同的格式化器,否則可能會拋出System.Runtime.Serialization.SerializationException異常.
  • [Serializable]特性: 默認自定義的類型是無法被序列化的,需要使用 [Serializable] 特性來實現(xiàn)序列化與反序列化,關于此特性更多的內(nèi)容見下節(jié);

通過上面的示例,我們往流中寫入了一個對象,那么可以寫入兩個,甚至多個不同的對象么?答案是肯定的,我們還是用代碼測試一下;

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)建一個流
        MemoryStream ms = new MemoryStream();
        //創(chuàng)建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化為二進制流
        bf.Serialize(ms, hero_ins);
        bf.Serialize(ms, soldier_ins);

        ms.Position = 0;
        hero_ins = null;
        soldier_ins = null;

        //從數(shù)據(jù)流中讀出數(shù)據(jù)
        //讀出的順序不能顛倒,因為是從ms的開端讀取,因此要按寫入的順序讀取
        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)

該特性不會被派生的子類繼承;

[Serializable] //注意這個關鍵字
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)建一個流
        MemoryStream ms = new MemoryStream();
        //創(chuàng)建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化為二進制流
        bf.Serialize(ms, hero_ins);
        bf.Serialize(ms, girl_ins);

        ms.Position = 0;
        hero_ins = null;
        girl_ins = null;

        //從數(shù)據(jù)流中讀出數(shù)據(jù)
        //讀出的順序不能顛倒,因為是從ms的開端讀取,因此要按寫入的順序讀取
        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());

    }
}

點擊運行后,果不其然會報 SerializationException 的一個錯誤:

SerializationException: Type 'GirlHero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

錯誤信息很明顯,我們的 GirlHero 類沒有標記 Serializable 特性,當我們給這個類也標記上該特性后,結(jié)果可以正常被打印;

我們在來看另外一種情況,只有派生類使用特性,基類不使用:

public class Hero
{
    public int id;
    public float attack;
    public float defence;
    public string name;
}

[Serializable]
public class GirlHero : Hero
{
    public int girlAge;
}

運行后報錯如下:

SerializationException: Type 'Hero' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

通過這個測試,我們可以知道:

  1. Serializable特性不會被繼承,每個派生類如果要想被序列化,需要單獨添加此特性字段.
  2. 派生類添加了 Serializable 特性,而基類不使用,那么從基類派生的任何類都無法被序列化.
    可以這么理解,基類如果無法被序列化,那么它的字段無法被序列化,派生類同樣包含該基類的字段,那么自然也是無法被序列化的.C#中的所有類都是繼承自 System.Object 類,這個類已經(jīng)應用了 Serializable 特性.

2. [NonSerialized]

在默認情況下,序列化會讀取對象的所有字段,無論這些字段生命的訪問權限是 public 還是 private, 如果我們有些敏感字段或者計算屬性不想被序列化,有沒有辦法呢?
在不想被序列化的字段上面使用 NonSerialized 屬性即可;

[Serializable] //注意這個關鍵字
public class Hero
{
    public int id;
    [NonSerialized]
    public float attack;
    public float defence;
    public string name;
    
}

使用上面Test的腳本,打印結(jié)果如下:

NonSerialized特性的使用

我們可以看到反序列化后,被標記為 NonSerialized 特性的字段值變?yōu)榱?,這是由于
attack 字段不能被序列化,它的值99并不會寫入到流中,因此被反序列化后,其余字段都能夠被正常賦值,該字段由于從流中讀取不到對應的值,只能設置為0;
那么能不能在反序列化的時候,把正確的值賦值回去呢?答案是肯定的,我們下節(jié)再來解決這個問題,我們繼續(xù)查看 NonSerialized 的繼承特點;

[Serializable] //注意這個關鍵字
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)建一個流
        MemoryStream ms = new MemoryStream();
        //創(chuàng)建格式化器
        BinaryFormatter bf = new BinaryFormatter();
        //序列化為二進制流
        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特性可以被繼承

通過上面的測試可以得知: [NonSerialized] 特性可以被派生類繼承;


控制序列化和反序列化的流程

在上一節(jié)提出的問題,對于 NonSerialized 修飾的字段,在反序列化的時候應該如何賦值,以及如果我們想在序列化和反序列化之前和之后做些操作,應該怎么實現(xiàn)?

[Serializable]
public class GirlHero : Hero
{
    public int girlAge;

    [OnDeserialized]
    private void CaculateAttack(StreamingContext context)
    {
        this.attack = 1000;
    }
}

在上一節(jié)的代碼基礎上,我們對 GirlHero 做了上面的改動,增加了一個 CaculateAttack 方法,并且使用了 [OnDeserialized] 特性,我們再來看打印結(jié)果:

控制反序列化

通過這樣的方法和特性我們對 attack 字段在反序列化的時候進行了賦值;
從特性的名字可以看出,是在反序列化過程完成后調(diào)用所修飾的方法,還有其他三個相關特性我們一起來看看;

1. 序列化與反序列化過程的方法特性

  • OnSerializing :格式化器在序列化對象字段之前,調(diào)用該特性修飾的方法.
  • OnSerialized :格式化器在序列化對象字段之后,調(diào)用該特性修飾的方法.
  • OnDeserializing :格式化器在反序列化對象字段之前,調(diào)用該特性修飾的方法.
  • OnDeserialized ::格式化器在反序列化對象字段之后,調(diào)用該特性修飾的方法.

這幾個特性是在 System.Runtime.Serialization 命名空間下,共同點是用來修飾類型中定義的方法;注意他們的調(diào)用時機.

2. StreamingContext

在上面的實例代碼中,可以看到方法參數(shù)是一個 StreamingContext 類,這個類是序列化與反序列化時流的上下文,我們通過程序集可以看到,這個類型是一個值類型.

public struct StreamingContext
{
    //調(diào)用方定義的附加上下文引用,一般為空
    public object Context {
        get;
    }
    //用來標記序列化和反序列對象的來源和目的地 
    public StreamingContextStates State {
        get;
    }
    //構造方法
    public StreamingContext (StreamingContextStates state);

    public StreamingContext (StreamingContextStates state, object additional);
    //重載System.Object方法
    public override bool Equals (object obj);

    public override int GetHashCode ();
}

通過State的屬性我們可以查看序列化和反序列化時對應的來源和目的地,更多的信息請查閱這里:StreamingContextStates枚舉
我們在上面序列化時使用的格式化器的 Context 屬性就是 StreamingContext, 它的 State 屬性默認是All,我們也可以在創(chuàng)建格式化器的時候手動指定 State 的類型來滿足不同的需求,比如:

//指定state類型,深度克隆一個對象
BinaryFormatter bf = new BinaryFormatter();
bf.Context = new StreamingContext(StreamingContextStates.Clone);

Unity的Inspector

在屬性監(jiān)視板中可以看到游戲腳本中某個對象的信息,這些字段和值并不是Unity調(diào)用游戲腳本中的C#接口獲取的,而是通過顯示對象的反序列化得到這些屬性數(shù)值,然后在面板中展示出來;


Unity的Prefab

Prefab是Unity中很重要的一種資源類型,真正實現(xiàn)了游戲?qū)ο蟮目寺?預制體是游戲?qū)ο蠛徒M件經(jīng)過序列化后得到的文件,它的格式可以是二進制的也可以是文本文件,可以通過下面的選項來設置:

資源格式設置

它的特點如下:

  • 可以被放入多個場景中,也可以在一個場景中放入多個
  • 在場景中增加一個Prefab,就實例化了一個該Prefab的實例
  • 所有的Prefab實例都是Prefab的克隆,因此在運行中生成Prefab實例的話可以看到這些實例會帶有(Clone)的標記
  • 只要Prefab的原型發(fā)生了變化,場景中所有的prefab實例都會發(fā)生變化

腳本創(chuàng)建Prefab實例我們都是通過Instantiate方法:

public static Object Instantiate (Object original, Vector3 position, Quaternion rotation)

在該方法內(nèi)部,會首先將參數(shù)original所引用的游戲?qū)ο笮蛄谢?得到序列化流后,再使用反序列化機制將這個序列化流生成一個新的游戲?qū)ο?可以說是對象的克隆操作;


Unity在System.Runtime.Serialization命名空間下定義了一個FormatterServices的類型,只包含一些靜態(tài)方法,用來輔助序列化與反序列化的過程;

序列化過程

  1. 調(diào)用FormatterServices的 GetSerializableMembers ;
//兩個重載版本
//type: 正在序列化或克隆的類型
//context: 發(fā)生序列化的上下文
//MemberInfo[]: 返回類型對象的數(shù)組,每一個元素都對應一個可以成員字段的名稱
public static MemberInfo[] GetSerializableMembers(Type type, StreamingContext context)
public static MemberInfo[] GetSerializableMembers(Type type)
  1. 調(diào)用FormatterServices的 GetObjectData ;
//obj: 表示要寫入序列化程序的對象實例
//members: 代表的是第一步提取的成員字段的名稱
//Object[]: 返回的是對應members中每個元素表示的字段對應的值,理解為Value的集合
public static Object[] GetObjectData(Object obj, MemberInfo[] members)
  1. 經(jīng)過前兩個步驟獲取了對象的成員和其對應的值,這一步先把程序集標識以及類型的完整名稱寫入流中.
  2. 格式化器遍歷第一步與第二步得到的數(shù)組獲取成員名稱和其對應的值,將這些信息寫入流中.

反序列化過程

  1. 格式化器從流中讀取程序集標識和完整的類型名稱,然后調(diào)用FormatterServices的 GetTypeFromAssembly ;
//assem: 讀取到的程序集標識
//name: 完整的類型名稱
//Type: 返回值便是反序列化對象的實際類型
public static Type GetTypeFromAssembly(Assembly assem, string name)
  1. 獲取了對象的類型后,接下來就是要在為新的對象分配一塊內(nèi)存空間,調(diào)用FormatterServices的 GetUninitializedObject ;
//為指定類型分配內(nèi)存空間
public static Object GetUninitializedObject(Type type)

需要注意的是,此時還沒有調(diào)用構造函數(shù),對象的所有字節(jié)都被初始化為 null 或者 0 ;

  1. 分配好內(nèi)存空間后,還是調(diào)用FormatterServices的 GetSerializableMembers 構造并初始化一個新的 MemberInfo 數(shù)組;
    這個方法的說明見序列化過程的第一步;
    調(diào)用方法后獲取該類型的所有成員字段名稱的集合 MemberInfo[] members ;
  2. 獲取到字段信息后,這一步就要獲取字段對應數(shù)組的信息;格式化器會根據(jù)流中包含的數(shù)據(jù)創(chuàng)建一個 Object 數(shù)組,對其進行初始化;
    到了這一步,你就有了一個未初始化的對象,一個成員變量集合和對應數(shù)值的集合;
  3. 這一步就要調(diào)用FormatterServices的 PopulateObjectMembers 方法對實例對象初始化;
//obj: 表示剛才創(chuàng)建要被初始化的對象實例
//members: 對象需要被填充的成員或者屬性
//data: 對象需要被填充的成員或者屬性對應的數(shù)值
//Object: 返回一個初始化好的實例對象
public static Object PopulateObjectMembers(Object obj, MemberInfo[] members, Object[] data)

參考文章: <<Unity3D腳本編程>> 陳嘉棟

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 11,203評論 0 24
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化,必須實現(xiàn)Serializable接口。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,555評論 1 3
  • 前言 Prefab,也就是大家熟知的預制件(本文中,我們依然使用它的英文名字——Prefab),它是Unity中一...
    windknife閱讀 21,738評論 9 22
  • 一、序列化 1、序列化的作用 Java平臺允許我們在內(nèi)存中創(chuàng)建可復用的Java對象,但一般情況下,只有當JVM處于...
    慕凌峰閱讀 4,365評論 0 8
  • 【小小陪伴】20170609學習力踐行記錄D25:早上,我去醫(yī)院照顧姥爺之前,把妮妮送到姥姥那,她問我“媽媽,姥爺...
    睿依show閱讀 153評論 0 0

友情鏈接更多精彩內(nèi)容