文章目錄
- 前言
- 1 項目結構
- 1.1 整體
- 1.2 代碼
- 2 實現
- 2.1 Processor
- 2.1.1 BaseType
- 2.1.2 CollectionType
- 2.1.3 CustomType
- 2.2 ByteFormatter
- 2.3 ByteHelper
- 3 使用
前言
? BinaryFormatter 類可以將 C# 類對象快速轉換為字節數組數據。
? 在網絡開發時,不會使用 BinaryFormatter 進行數據序列化和反序列化。因為客戶端和服務端的開發語言多數情況下不同,BinaryFormatter 序列化的數據無法兼容其它語言。
? 因此,需要自定義序列化方式。
? BinaryFormatter 參考鏈接:2023-05-27 Unity 2進制4——類對象的序列化與反序列化_unity 二進制序列化-CSDN博客。
- 項目鏈接:https://github.com/zheliku/ByteHelper。
- Unity 版本:6000.0.42f1。
1 項目結構
1.1 整體

- Scenes(示例場景)
- Scripts(腳本)
- ByteHelper(工具腳本)
- Test(測試腳本)
1.2 代碼

- Processor(存放對應類型的 Processor)
- BaseType(基本類型的 Processor)
- CollectionType(集合類型的 Processor)
- CustomType(自定義類型的 Processor)
- Processor.cs(抽象基類,用于處理對象和字節數組之間的轉換)
- ByteFormatter.cs(存儲所有 Processor,并依據類型進行 Write 與 Read)
- ByteHelper.cs(封裝序列化 API)
- ReflectionExtension.cs(反射方法拓展)
2 實現
2.1 Processor
? 一個 Processor 用于處理一種類型的序列化,其包含以下 3 種方法:
-
GetBytesLength
獲取對象在字節數組中占用的字節數。
-
Write
將對象寫入 bytes 數組。
-
Read
從 bytes 數組中讀取對象。
// 抽象類 Processor,用于處理對象和字節數組之間的轉換
public abstract class Processor
{public abstract int GetBytesLength(object value);public abstract int Write(byte[] bytes, object value, int index);public abstract int Read(byte[] bytes, int index, out object value);
}
? 使用泛型版本標識每個 Processor 處理的類型:
public abstract class Processor<TValue> : Processor
{public abstract int GetBytesLength(TValue value);public abstract int Write(byte[] bytes, TValue value, int index);public abstract int Read(byte[] bytes, int index, out TValue value);public override int GetBytesLength(object value){return GetBytesLength((TValue) value);}public override int Write(byte[] bytes, object value, int index){return Write(bytes, (TValue) value, index);}public override int Read(byte[] bytes, int index, out object value){int result = Read(bytes, index, out TValue typedValue);value = typedValue;return result;}
}
2.1.1 BaseType
? 以 int、string 類型為例:
IntProcessor
-
GetBytesLength
int 類型使用 4 個子節存儲,可直接返回 4,也可使用
sizeof(int)
。 -
Write
直接轉換為子節,寫入 bytes 中的 index 位置。返回值為寫入后下一處的位置。
-
Read
直接將 bytes 中 index 位置的數據讀取。返回值為讀取后下一處的位置。
public class IntProcessor : Processor<int>
{public override int GetBytesLength(int value){return sizeof(int);}public override int Write(byte[] bytes, int value, int index){BitConverter.GetBytes(value).CopyTo(bytes, index);return index + sizeof(int);}public override int Read(byte[] bytes, int index, out int value){value = BitConverter.ToInt32(bytes, index);return index + sizeof(int);}
}
StringProcessor
? string 類型長度可變,因此需要先寫入長度(int 類型),再寫入內容。
- GetBytesLength:int 長度 + 字符串長度。
- Write:先寫入長度,后寫入內容。
- Read:先讀取長度,后讀取內容。
public class StringProcessor : Processor<string>
{public override int GetBytesLength(string value){return sizeof(int) + value.Length;}public override int Write(byte[] bytes, string value, int index){BitConverter.GetBytes(value.Length).CopyTo(bytes, index);Encoding.UTF8.GetBytes(value).CopyTo(bytes, index + sizeof(int));return index + sizeof(int) + value.Length;}public override int Read(byte[] bytes, int index, out string value){int length = BitConverter.ToInt32(bytes, index);value = Encoding.UTF8.GetString(bytes, index + sizeof(int), length);return index + sizeof(int) + length;}
}
2.1.2 CollectionType
? 以 ICollectionProcessor 為例,泛型集合類型需要記錄集合本身的 Type 與元素的 Type,因此 ICollectionProcessor 具有 2 個泛型參數。
-
GetBytesLength
集合長度可變,因此也先寫入長度,后順序寫入集合元素。
-
Write
先寫入長度,后順序寫入集合元素。
使用
ByteFormatter.Write
方法,依據元素類型,自動調用對應的 Processor 寫入內容。 -
Read
先讀取長度,后順序讀取集合元素。
使用
ByteFormatter.Read
方法,依據元素類型,自動調用對應的 Processor 讀取內容。
public class ICollectionProcessor<TCollection, TValue> : Processor<TCollection> where TCollection : ICollection<TValue>
{public override int GetBytesLength(TCollection value){var length = sizeof(int);foreach (var item in value){length += ByteFormatter.GetBytesLength(item);}return length;}public override int Write(byte[] bytes, TCollection value, int index){int count = value.Count;// 寫長度BitConverter.GetBytes(count).CopyTo(bytes, index);index += sizeof(int); // 留 1 個 int 位置用于寫長度// 寫內容foreach (var item in value){index = ByteFormatter.Write(bytes, item, index);}return index;}public override int Read(byte[] bytes, int index, out TCollection value){// 1. 讀取長度(元素數量)int length = BitConverter.ToInt32(bytes, index);index += sizeof(int);// 2. 讀取內容value = (TCollection) Activator.CreateInstance(typeof(TCollection));for (int i = 0; i < length; i++){index = ByteFormatter.Read(bytes, index, typeof(TValue), out var item);value.Add((TValue) item);}return index;}
}
2.1.3 CustomType
? 自定義類型默認序列化所有的字段,且需要添加 [ByteSerializable]
特性。
-
GetBytesLength
長度可變,因此也先寫入長度,后順序寫入字段。
-
Write
先寫入長度,后使用反射獲取所有字段信息,依次寫入字段內容。
反射方法
value.GetFieldValues()
在 ReflectionExtension.cs 中封裝,默認獲取所有字段的值。使用
ByteFormatter.Write
方法,依據字段類型,自動調用對應的 Processor 寫入。 -
Read
先讀取長度,后使用反射獲取所有字段信息,依次讀取字段內容。
反射方法
obj.GetFieldInfos()
在 ReflectionExtension.cs 中封裝,默認獲取所有字段的信息。使用
ByteFormatter.Read
方法,依據字段類型,自動調用對應的 Processor 讀取。考慮到值類型,在讀取時需要裝箱,否則反射賦值時,會將內容賦值給 fieldInfo.SetValue 方法中的臨時變量。
public class CustomTypeProcessor<TValue> : Processor<TValue>
{public override int GetBytesLength(TValue value){return sizeof(int) + value.GetFieldValues().Sum(v => ByteFormatter.GetBytesLength(v));}public override int Write(byte[] bytes, TValue value, int index){// 先寫長度,以便讀取BitConverter.GetBytes(GetBytesLength(value)).CopyTo(bytes, index);index += sizeof(int);var fieldValues = value.GetFieldValues();foreach (var fieldValue in fieldValues){index = ByteFormatter.Write(bytes, fieldValue, index);}return index;}public override int Read(byte[] bytes, int index, out TValue value){var obj = (object) Activator.CreateInstance<TValue>(); // 裝箱,以防 TValue 為值類型index += sizeof(int);var fieldInfos = obj.GetFieldInfos();foreach (var fieldInfo in fieldInfos){index = ByteFormatter.Read(bytes, index, fieldInfo.FieldType, out var v);fieldInfo.SetValue(obj, v);}value = (TValue) obj; // 拆箱,還原 valuereturn index;}
}
2.2 ByteFormatter
? 管理所有類型的 Processor,使用字典存儲預置基礎類型:
public class ByteFormatter
{// 定義字典,用于存儲預置類型的處理器public static readonly Dictionary<Type, object> PRIMITIVE_PROCESSORS = new Dictionary<Type, object>(){{ typeof(int), new IntProcessor() },{ typeof(short), new ShortProcessor() },{ typeof(long), new LongProcessor() },{ typeof(float), new FloatProcessor() },{ typeof(double), new DoubleProcessor() },{ typeof(bool), new BoolProcessor() },{ typeof(char), new CharProcessor() },{ typeof(byte), new ByteProcessor() },{ typeof(string), new StringProcessor() },};...
}
? 依據傳入類型,提供對應的 Processor,若不存在則創建。
- 在預置類型的字典中,直接讀取
public class ByteFormatter
{// 根據類型獲取對應的處理器public static Processor<T> GetProcessor<T>(){return (Processor<T>) GetProcessor(typeof(T));}public static Processor GetProcessor(Type type){// 在預置類型的字典中,直接讀取if (PRIMITIVE_PROCESSORS.TryGetValue(type, out object value)) {return (Processor) value;}}
}
-
優先處理字典類型,并且先處理 KeyValuePair。
若字典中存在,則直接讀取,否則創建 Processor 添加到字典中再返回。
// 如果類型是 KeyValuePair,則使用 KeyValuePairProcessor
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{var processorType = typeof(KeyValuePairProcessor<,>).MakeGenericType(type.GetGenericArguments());var processor = (Processor) Activator.CreateInstance(processorType);PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中return processor;
}// 如果類型是字典,則使用 IDictionaryProcessor
if (type.IsAssignableToGenericInterface(typeof(IDictionary<,>)))
{var processorType = typeof(IDictionaryProcessor<,,>).MakeGenericType(type, type.GetGenericArguments()[0], type.GetGenericArguments()[1]);var processor = (Processor) Activator.CreateInstance(processorType);PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中return processor;
}
-
對于集合類型與用戶自定義類型,同理。
對于用戶自定義類型,需要繼承
[ByteSerializable]
特性。也可以根據需要自定義其他方式。
// 如果是集合類型,則使用 ICollectionProcessor
if (type.IsAssignableToGenericInterface(typeof(ICollection<>)))
{var processorType = typeof(ICollectionProcessor<,>).MakeGenericType(type, type.GetGenericArguments()[0]);var processor = (Processor) Activator.CreateInstance(processorType);PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中return processor;
}// 如果擁有 ByteSerializableAttribute 特性,則使用 CustomTypeProcessor
if (type.HasAttribute<ByteSerializableAttribute>())
{var processorType = typeof(CustomTypeProcessor<>).MakeGenericType(type);var processor = (Processor) Activator.CreateInstance(processorType);PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中return processor;
}
? 提供 Write 和 Read 方法,依據參數類型,自動獲取對應 Processor 處理。
public static int Write<T>(byte[] bytes, T value, int index)
{return GetProcessor<T>().Write(bytes, value, index);
}public static int Write(byte[] bytes, object value, int index)
{return GetProcessor(value.GetType()).Write(bytes, value, index);
}public static int Read<T>(byte[] bytes, int index, out T value)
{return GetProcessor<T>().Read(bytes, index, out value);
}public static int Read(byte[] bytes, int index, Type type, out object value)
{return GetProcessor(type).Read(bytes, index, out value);
}
2.3 ByteHelper
? 封裝序列化方法。
public class ByteHelper
{public const string EXTENSION = ".bytes";public static string BinarySavePath { get; set; } = Application.persistentDataPath + "/Binary";public static byte[] Serialize<TData>(TData data){var processor = ByteFormatter.GetProcessor<TData>();var bytes = new byte[processor.GetBytesLength(data)];processor.Write(bytes, data, 0);return bytes;}public static TData Deserialize<TData>(byte[] bytes){ByteFormatter.Read<TData>(bytes, 0, out var value);return value;}public static void SaveBytes<TData>(string filePath, TData data, string extension = EXTENSION){string fullPath = Path.Combine(BinarySavePath, filePath);fullPath = Path.ChangeExtension(fullPath, EXTENSION);var directory = Path.GetDirectoryName(fullPath);if (directory != null && !Directory.Exists(directory)){Directory.CreateDirectory(directory);}byte[] bytes = Serialize(data);File.WriteAllBytes(fullPath, bytes);#if UNITY_EDITORUnityEditor.AssetDatabase.Refresh();
#endif}public static TData LoadBytes<TData>(string filePath, string extension = ByteHelper.EXTENSION){string fullPath = Path.Combine(BinarySavePath, filePath);fullPath = Path.ChangeExtension(fullPath, EXTENSION);if (!File.Exists(fullPath)){ // 不存在文件,則警告,并返回默認值Debug.LogWarning($"ByteHelper: Can't find path \"{fullPath}\"");return default(TData);}byte[] bytes = File.ReadAllBytes(fullPath);return Deserialize<TData>(bytes);}
}
3 使用
? 以 CustomType 為例,打開 CustomType 場景,點擊 “TestScript” 物體,右側編輯你想要的數據。

? 運行場景,會自動序列化數據,對應腳本如下:
[Serializable] [ByteSerializable]
public class CustomData
{public int Id;private string _name;public List<int> List = new List<int>();public NestedData NestedData; // Supports nested typespublic string Name{get => _name;set => _name = value;}public override string ToString() {...}
}[Serializable] [ByteSerializable]
public struct NestedData
{public bool Bool;public override string ToString() {...}
}public class Test_CustomType : MonoBehaviour
{public Button Btn;public Text Text;[Header("Set Your CustomData")]public CustomData CustomData;public NestedData NestedData;private void Start(){CustomData.Name = "zheliku"; // 設置私有字段SerializeData();Text.text = "Serialize Data Success!";Btn.onClick.AddListener(OnClick);}// 序列化數據,保存到 CustomType 目錄下。private void SerializeData(){ByteHelper.SaveBytes($"CustomType/{nameof(CustomData)}", CustomData);ByteHelper.SaveBytes($"CustomType/{nameof(NestedData)}", NestedData);}// 點擊按鈕時,顯示反序列化數據public void OnClick(){Text.text = "CustomData: " + ByteHelper.LoadBytes<CustomData>($"CustomType/{nameof(CustomData)}") + "\n\n" +"NestedData: " + ByteHelper.LoadBytes<NestedData>($"CustomType/{nameof(NestedData)}");}
}
? 點擊按鈕,即可顯示反序列化數據:

? 點擊 “ByteHelper/Open Binary Folder” 可打開序列化數據存儲目錄。

? CustomType 保存在 “CustomType” 目錄下。

? CustomData 的序列化內容如下:

? 根據需要可自行擴展 Processor。