Unity的四种数据持久化方式

248 阅读45分钟

什么是数据持久化

        数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称  ;通俗来讲就是将数据存到硬盘,硬盘中数据读到游戏中,也就是传统意义上的存盘

数据持久化之PlayerPrefs

 概述

        PlayerPrefs,是Unity引擎内置的主要用来存储和读取玩家偏好设定的一个类,这是官方手册里写的它的主要用途。但其实它不只可以存储玩家的偏好设定,也可以用来存储简单的数据。

API及用法

        #region 存储相关
        //PlayerPrefs的数据存储 类似于键值对存储 一个键对应一个值
        //提供了存储3种数据的方法 int float string
        //键: string类型 
        //值:int float string 对应3种API

        PlayerPrefs.SetInt("myAge", 26);
        PlayerPrefs.SetFloat("myHeight", 188.5f);
        PlayerPrefs.SetString("myName", "Danny");

        //直接调用Set相关方法 只会把数据存到内存里
        //当游戏结束时 Unity会自动把数据存到硬盘中
        //如果游戏不是正常结束的 而是崩溃 数据是不会存到硬盘中的
        //只要调用该方法 就会马上存储到硬盘中
        PlayerPrefs.Save();

        //PlayerPrefs是有局限性的 它只能存3种类型的数据
        //如果你想要存储别的类型的数据 只能降低精度 或者上升精度来进行存储
        bool sex = true;
        PlayerPrefs.SetInt("sex", sex ? 1 : 0);

        //如果不同类型用同一键名进行存储 会进行覆盖
        PlayerPrefs.SetFloat("myAge", 20.2f);
        #endregion

        #region 读取相关
        //注意 运行时 只要你Set了对应键值对
        //即使你没有马上存储Save在本地
        //也能够读取出信息

        //int
        int age = PlayerPrefs.GetInt("myAge");
        print(age);
        //前提是 如果找不到myAge对应的值 就会返回函数的第二个参数 默认值
        age = PlayerPrefs.GetInt("myAge", 100);
        print(age);

        //float
        float height = PlayerPrefs.GetFloat("myHeight", 1000f);
        print(height);

        //string
        string name = PlayerPrefs.GetString("myName");
        print(name);

        //第二个参数 默认值 对于我们的作用
        //就是 在得到没有的数据的时候 就可以用它来进行基础数据的初始化

        //判断数据是否存在
        if( PlayerPrefs.HasKey("myName") )
        {
            print("存在myName对应的键值对数据");
        }
        #endregion

        #region 删除数据
        //删除指定键值对
        PlayerPrefs.DeleteKey("myAge");
        //删除所有存储的信息
        PlayerPrefs.DeleteAll();
        #endregion

电脑中存放的位置

PlayerPrefs将数据存储在了电脑的注册表中。

打开方式:

  • Win + r 输入regedit进入注册表界面
  • HKEY_CURRENT_USER/Software/Unity
  • 若你在编辑器中测试的,就进入UnityEditor,找到你工程的:公司/工程名 文件夹

优缺点 

 优点

        PlayerPrefs是数据持久化系列中最简单的一部分内容,简单快捷易懂

缺点

  • 重复工作繁多
  • 自定义数据类型都需要自己去实现读取功能,而且代码的相似度极高
  • 数据容易被修改、只要找到文件位置,就可以轻易的进行数据修改

主要用处

  • 单独使用他的原生功能
  • 适合存储一些对安全性要求不高的简单数据
  • 但也不能小看它,对他进行简单的封装也可以变的方便又安全

封装PlayerPrefs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// PlayerPrefs数据管理类 统一管理数据的存储和读取
/// </summary>
public class PlayerPrefsDataMgr
{
    private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();

    public static PlayerPrefsDataMgr Instance
    {
        get
        {
            return instance;
        }
    }

    private PlayerPrefsDataMgr()
    {

    }

    /// <summary>
    /// 存储数据
    /// </summary>
    /// <param name="data">数据对象</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    public void SaveData( object data, string keyName )
    {
        //就是要通过 Type 得到传入数据对象的所有的 字段
        //然后结合 PlayerPrefs来进行存储

        #region 第一步 获取传入数据对象的所有字段
        Type dataType = data.GetType();
        //得到所有的字段
        FieldInfo[] infos = dataType.GetFields();
        #endregion

        #region 第二步 自己定义一个key的规则 进行数据存储
        //我们存储都是通过PlayerPrefs来进行存储的
        //保证key的唯一性 我们就需要自己定一个key的规则

        //我们自己定一个规则
        // keyName_数据类型_字段类型_字段名
        #endregion

        #region 第三步 遍历这些字段 进行数据存储
        string saveKeyName = "";
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            //对每一个字段 进行数据存储
            //得到具体的字段信息
            info = infos[i];
            //通过FieldInfo可以直接获取到 字段的类型 和字段的名字
            //字段的类型 info.FieldType.Name
            //字段的名字 info.Name;

            //要根据我们定的key的拼接规则 来进行key的生成
            //Player1_PlayerInfo_Int32_age
            saveKeyName = keyName + "_" + dataType.Name + 
                "_" + info.FieldType.Name + "_" + info.Name;

            //现在得到了Key 按照我们的规则
            //接下来就要来通过PlayerPrefs来进行存储
            //如何获取值
            //info.GetValue(data)
            //封装了一个方法 专门来存储值 
            SaveValue(info.GetValue(data), saveKeyName);
        }

        PlayerPrefs.Save();
        #endregion
    }

    private void SaveValue(object value, string keyName)
    {
        //直接通过PlayerPrefs来进行存储了
        //就是根据数据类型的不同 来决定使用哪一个API来进行存储
        //PlayerPrefs只支持3种类型存储 
        //判断 数据类型 是什么类型 然后调用具体的方法来存储
        Type fieldType = value.GetType();

        //类型判断
        //是不是int
        if( fieldType == typeof(int) )
        {
            //为int数据加密
            int rValue = (int)value;
            rValue += 10;
            PlayerPrefs.SetInt(keyName, rValue);
        }
        else if (fieldType == typeof(float))
        {
            PlayerPrefs.SetFloat(keyName, (float)value);
        }
        else if (fieldType == typeof(string))
        {
            PlayerPrefs.SetString(keyName, value.ToString());
        }
        else if (fieldType == typeof(bool))
        {
            //自己顶一个存储bool的规则
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }
        //如何判断 泛型类的类型呢
        //通过反射 判断 父子关系
        //这相当于是判断 字段是不是IList的子类
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //父类装子类
            IList list = value as IList;
            //先存储 数量 
            PlayerPrefs.SetInt(keyName, list.Count);
            int index = 0;
            foreach (object obj in list)
            {
                //存储具体的值
                SaveValue(obj, keyName + index);
                ++index;
            }
        }
        //判断是不是Dictionary类型 通过Dictionary的父类来判断
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            //父类装自来
            IDictionary dic = value as IDictionary;
            //先存字典长度
            PlayerPrefs.SetInt(keyName, dic.Count);
            //遍历存储Dic里面的具体值
            //用于区分 表示的 区分 key
            int index = 0;
            foreach (object key in dic.Keys)
            {
                SaveValue(key, keyName + "_key_" + index);
                SaveValue(dic[key], keyName + "_value_" + index);
                ++index;
            }
        }
        //基础数据类型都不是 那么可能就是自定义类型
        else
        {
            SaveData(value, keyName);
        }
    }

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="type">想要读取数据的 数据类型Type</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    /// <returns></returns>
    public object LoadData( Type type, string keyName )
    {
        //不用object对象传入 而使用 Type传入
        //主要目的是节约一行代码(在外部)
        //假设现在你要 读取一个Player类型的数据 如果是object 你就必须在外部new一个对象传入
        //现在有Type的 你只用传入 一个Type typeof(Player) 然后我在内部动态创建一个对象给你返回出来
        //达到了 让你在外部 少写一行代码的作用

        //根据你传入的类型 和 keyName
        //依据你存储数据时  key的拼接规则 来进行数据的获取赋值 返回出去

        //根据传入的Type 创建一个对象 用于存储数据
        object data = Activator.CreateInstance(type);
        //要往这个new出来的对象中存储数据 填充数据
        //得到所有字段
        FieldInfo[] infos = type.GetFields();
        //用于拼接key的字符串
        string loadKeyName = "";
        //用于存储 单个字段信息的 对象
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            info = infos[i];
            //key的拼接规则 一定是和存储时一模一样 这样才能找到对应数据
            loadKeyName = keyName + "_" + type.Name +
                "_" + info.FieldType.Name + "_" + info.Name;

            //有key 就可以结合 PlayerPrefs来读取数据
            //填充数据到data中 
            info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
        }
        return data;
    }

    /// <summary>
    /// 得到单个数据的方法
    /// </summary>
    /// <param name="fieldType">字段类型 用于判断 用哪个api来读取</param>
    /// <param name="keyName">用于获取具体数据</param>
    /// <returns></returns>
    private object LoadValue(Type fieldType, string keyName)
    {
        //根据 字段类型 来判断 用哪个API来读取
        if( fieldType == typeof(int) )
        {
            //解密 减10
            return PlayerPrefs.GetInt(keyName, 0) - 10;
        }
        else if (fieldType == typeof(float))
        {
            return PlayerPrefs.GetFloat(keyName, 0);
        }
        else if (fieldType == typeof(string))
        {
            return PlayerPrefs.GetString(keyName, "");
        }
        else if (fieldType == typeof(bool))
        {
            //根据自定义存储bool的规则 来进行值的获取
            return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
        }
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //得到长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个List对象 来进行赋值
            //用了反射中双A中 Activator进行快速实例化List对象
            IList list = Activator.CreateInstance(fieldType) as IList;
            for (int i = 0; i < count; i++)
            {
                //目的是要得到 List中泛型的类型 
                list.Add(LoadValue(fieldType.GetGenericArguments()[0], keyName + i));
            }
            return list;
        }
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            //得到字典的长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个字典对象 用父类装子类
            IDictionary dic = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] kvType = fieldType.GetGenericArguments();
            for (int i = 0; i < count; i++)
            {
                dic.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                         LoadValue(kvType[1], keyName + "_value_" + i));
            }
            return dic;
        }
        else
        {
            return LoadData(fieldType, keyName);
        }

    }
}


数据持久化之XML

XML是什么

  • 全称:可扩展性标记语言(EXtensible Markup Language)
  • XML是国际通用的
  • 他是被设计来用于传输和存储数据的一种文本特殊格式
  • 文件名后缀一般为.xml

读取XML信息

C#读取XML的方法有几种 

  • 1.XmlDocument    (把数据加载到内存中,方便读取)
  • 2.XmlTextReader  (以流形式加载,内存占用更少,但是是单向只读,使用不是特别方便,除非有特殊需求,否则不会使用)
  • 3.Linq  
  • 使用XmlDocument类读取是较方便最容易理解和操作的方法

读取xml文件信息

XmlDocument xml = new XmlDocument();
//通过XmlDocument读取xml文件 有两个API
//1.直接根据xml字符串内容 来加载xml文件
//存放在Resorces文件夹下的xml文件加载处理
TextAsset asset = Resources.Load<TextAsset>("TestXml");
print(asset.text);
//通过这个方法 就能够翻译字符串为xml对象
xml.LoadXml(asset.text);

//2.是通过xml文件的路径去进行加载
xml.Load(Application.streamingAssetsPath + "/TestXml.xml");

 读取元素和属性信息

//节点信息类
//XmlNode 单个节点信息类
//节点列表信息
//XmlNodeList 多个节点信息类

//获取xml当中的根节点
XmlNode root = xml.SelectSingleNode("Root");
//再通过根节点 去获取下面的子节点
XmlNode nodeName = root.SelectSingleNode("name");
//如果想要获取节点包裹的元素信息 直接 .InnerText
print(nodeName.InnerText);

XmlNode nodeAge = root.SelectSingleNode("age");
print(nodeAge.InnerText);

XmlNode nodeItem = root.SelectSingleNode("Item");
//第一种方式 直接 中括号获取信息
print(nodeItem.Attributes["id"].Value);
print(nodeItem.Attributes["num"].Value);
//第二种方式 
print(nodeItem.Attributes.GetNamedItem("id").Value);
print(nodeItem.Attributes.GetNamedItem("num").Value);

//这里是获取 一个节点下的同名节点的方法
XmlNodeList friendList = root.SelectNodes("Friend");

//遍历方式一:迭代器遍历
//foreach (XmlNode item in friendList)
//{
//    print(item.SelectSingleNode("name").InnerText);
//    print(item.SelectSingleNode("age").InnerText);
//}
//遍历方式二:通过for循环遍历
//通过XmlNodeList中的 成员变量 Count可以得到 节点数量
for (int i = 0; i < friendList.Count; i++)
{
    print(friendList[i].SelectSingleNode("name").InnerText);
    print(friendList[i].SelectSingleNode("age").InnerText);
}

总结

1.读取XML文件

  • XmlDocument xml = new XmlDocument();
  • 读取文本方式1   xml.LoadXml(传入xml文本字符串)
  • 读取文本方式2   xml.Load(传入路径)

2.读取元素和属性

  • 获取单个节点 : XmlNode node = xml.SelectSingleNode(节点名)
  • 获取多个节点 : XmlNodeList nodeList = xml.SelectNodes(节点名)
  • 获取节点元素内容:node.InnerText
  • 获取节点元素属性:

        1.item.Attributes["属性名"].Value
2.item.Attributes.GetNamedItem("属性名").Value

通过迭代器遍历或者循环遍历XmlNodeList对象 可以获取到各单个元素节点

写入XML信息

选择存储目录

 注意:存储xml文件 在Unity中一定是使用各平台都可读可写可找到的路径

  1. Resources 可读 不可写 打包后找不到  ×
  2.  Application.streamingAssetsPath 可读 PC端可写 找得到  ×
  3.  Application.dataPath 打包后找不到  ×
  4. Application.persistentDataPath 可读可写找得到   √
string path = Application.persistentDataPath + "/TextXml.xml";
print(Application.persistentDataPath);

存储xml文件

  • 关键类 XmlDocument 用于创建节点 存储文件
  • 关键类 XmlDeclaration 用于添加版本信息
  • 关键类 XmlElement 节点类

存储有5步

//1.创建文本对象
XmlDocument xml = new XmlDocument();

//2.添加固定版本信息
//这一句代码 相当于就是创建<?xml version="1.0" encoding="UTF-8"?>这句内容
XmlDeclaration xmlDec = xml.CreateXmlDeclaration("1.0", "UTF-8", "");
//创建完成过后 要添加进入 文本对象中
xml.AppendChild(xmlDec);

//3.添加根节点
XmlElement root = xml.CreateElement("Root");
xml.AppendChild(root);

//4.为根节点添加子节点
//加了一个 name子节点
XmlElement name = xml.CreateElement("name");
name.InnerText = "Danny";
root.AppendChild(name);

XmlElement atk = xml.CreateElement("atk");
atk.InnerText = "10";
root.AppendChild(atk);

XmlElement listInt = xml.CreateElement("listInt");
for (int i = 1; i <= 3; i++)
{
    XmlElement childNode = xml.CreateElement("int");
    childNode.InnerText = i.ToString();
    listInt.AppendChild(childNode);
}
root.AppendChild(listInt);

XmlElement itemList = xml.CreateElement("itemList");
for (int i = 1; i <= 3; i++)
{
    XmlElement childNode = xml.CreateElement("Item");
    //添加属性
    childNode.SetAttribute("id", i.ToString());
    childNode.SetAttribute("num", (i * 10).ToString());
    itemList.AppendChild(childNode);
}
root.AppendChild(itemList);

//5.保存
xml.Save(path);

修改xml文件

//1.先判断是否存在文件
if( File.Exists(path) )
{
    //2.加载后 直接添加节点 移除节点即可
    XmlDocument newXml = new XmlDocument();
    newXml.Load(path);

    //修改就是在原有文件基础上 去移除 或者添加
    //移除
    XmlNode node;// = newXml.SelectSingleNode("Root").SelectSingleNode("atk");
    //这种是一种简便写法 通过/来区分父子关系
    node = newXml.SelectSingleNode("Root/atk");
    //得到自己的父节点
    XmlNode root2 = newXml.SelectSingleNode("Root");
    //移除子节点方法
    root2.RemoveChild(node);

    //添加节点
    XmlElement speed = newXml.CreateElement("moveSpeed");
    speed.InnerText = "20";
    root2.AppendChild(speed);

    //改了记得存
    newXml.Save(path);
}

总结

1.路径选取
在运行过程中存储 只能往可写且能找到的文件夹存储
故 选择了Application.persistentDataPath

2.存储xml关键类
XmlDocument  文件  
创建节点 CreateElement
创建固定内容方法 CreateXmlDeclaration
添加节点 AppendChild
保存 Save
XmlDeclaration 版本
XmlElement 元素节点  
设置属性方法SetAttribute

3.修改
RemoveChild移除节点

XML序列化

什么是序列化和反序列化

序列化: 把对象转化为可传输的字节序列过程称为序列化(序列化就是把想要存储的内容转换为字节序列用于存储或传递)
反序列化: 把字节序列还原为对象的过程称为反序列化(反序列化就是把存储或收到的字节序列信息解析读取出来使用)

xml序列化

//1.第一步准备一个数据结构类
Lesson1Test lt = new Lesson1Test();
//2.进行序列化
//  关键知识点
//  XmlSerializer 用于序列化对象为xml的关键类
//  StreamWriter 用于存储文件  
//  using 用于方便流对象释放和销毁

//第一步:确定存储路径
string path = Application.persistentDataPath + "/Lesson1Test.xml";
print(Application.persistentDataPath);
//第二步:结合 using知识点 和 StreamWriter这个流对象 来写入文件
// 括号内的代码:写入一个文件流 如果有该文件 直接打开并修改 如果没有该文件 直接新建一个文件
// using 的新用法 括号当中包裹的声明的对象 会在 大括号语句块结束后 自动释放掉 
// 当语句块结束 会自动帮助我们调用 对象的 Dispose这个方法 让其进行销毁
// using一般都是配合 内存占用比较大 或者 有读写操作时  进行使用的 
using ( StreamWriter stream = new StreamWriter(path) )
{
    //第三步:进行xml文件序列化
    XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));
    //这句代码的含义 就是通过序列化对象 对我们类对象进行翻译 将其翻译成我们的xml文件 写入到对应的文件中
    //第一个参数 : 文件流对象 
    //第二个参数: 想要备翻译 的对象
    //注意 :翻译机器的类型 一定要和传入的对象是一致的 不然会报错
    s.Serialize(stream, lt);
}

自定义节点名 或 设置属性

//可以通过特性 设置节点或者设置属性 并且修改名字
[XmlArray("IntList")]
[XmlArrayItem("Int32")]
public List<int> listInt;

[XmlElement("testPublic123123")]
public int testPublic;

[XmlAttribute("Test1")]
public int test1 = 1;

总结

序列化流程

  1. 有一个想要保存的类对象
  2. 使用XmlSerializer 序列化该对象
  3. 通过StreamWriter 配合 using将数据存储 写入文件

注意:

  1. 只能序列化公共成员
  2. 不支持字典序列化
  3. 可以通过特性修改节点信息 或者设置属性信息
  4. Stream相关要配合using使用

XML反序列化

#region 知识点一 判断文件是否存在
string path = Application.persistentDataPath + "/Lesson1Test.xml";
if( File.Exists(path) )
{
    #region 知识点二 反序列化
    //关键知识
    // 1.using 和 StreamReader
    // 2.XmlSerializer 的 Deserialize反序列化方法

    //读取文件
    using (StreamReader reader = new StreamReader(path))
    {
        //产生了一个 序列化反序列化的翻译机器
        XmlSerializer s = new XmlSerializer(typeof(Lesson1Test));
        Lesson1Test lt = s.Deserialize(reader) as Lesson1Test;
    }
    #endregion
}
#endregion

#region 总结
//1.判断文件是否存在 File.Exists
//2.文件流获取 StreamReader reader = new StreamReader(path)
//3.根据文件流 XmlSerializer通过Deserialize反序列化 出对象

//注意:List对象 如果有默认值 反序列化时 不会清空 会往后面添加
#endregion

自定义类继承IXmlSerializable

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using UnityEngine;

public class TestLesson3 : IXmlSerializable
{
    public int test1;
    public string test2;

    //返回结构
    public XmlSchema GetSchema()
    {
        return null;
    }

    //反序列化时 会自动调用的方法
    public void ReadXml(XmlReader reader)
    {
        //在里面可以自定义反序列化 的规则
        //1.读属性
        //this.test1 = int.Parse(reader["test1"]);
        //this.test2 = reader["test2"];

        //2.读节点
        //方式一
        //reader.Read();//这时是读到的test1节点
        //reader.Read();//这时是读到的test1节点包裹的内容
        //this.test1 = int.Parse(reader.Value);//得到当前内容的值
        //reader.Read();//这时读到的是尾部包裹节点
        //reader.Read();//这时是读到的test2节点
        //reader.Read();//这时是读到的test2节点包裹的内容
        //this.test2 = reader.Value;
        //方式二
        //while(reader.Read())
        //{
        //    if( reader.NodeType == XmlNodeType.Element )
        //    {
        //        switch (reader.Name)
        //        {
        //            case "test1":
        //                reader.Read();
        //                this.test1 = int.Parse(reader.Value);
        //                break;
        //            case "test2":
        //                reader.Read();
        //                this.test2 = reader.Value;
        //                break;
        //        }
        //    }
        //}

        //3.读包裹元素节点
        XmlSerializer s = new XmlSerializer(typeof(int));
        XmlSerializer s2 = new XmlSerializer(typeof(string));
        //跳过根节点
        reader.Read();
        reader.ReadStartElement("test1");
        test1 = (int)s.Deserialize(reader);
        reader.ReadEndElement();

        reader.ReadStartElement("test2");
        test2 = s2.Deserialize(reader).ToString();
        reader.ReadEndElement();
    }

    //序列化时 会自动调用的方法
    public void WriteXml(XmlWriter writer)
    {
        //在里面可以自定义序列化 的规则

        //如果要自定义 序列化的规则 一定会用到 XmlWriter中的一些方法 来进行序列化
        //1.写属性
        //writer.WriteAttributeString("test1", this.test1.ToString());
        //writer.WriteAttributeString("test2", this.test2);

        //2.写节点
        //writer.WriteElementString("test1", this.test1.ToString());
        //writer.WriteElementString("test2", this.test2);

        //3.写包裹节点
        XmlSerializer s = new XmlSerializer(typeof(int));
        writer.WriteStartElement("test1");
        s.Serialize(writer, test1);
        writer.WriteEndElement();

        XmlSerializer s2 = new XmlSerializer(typeof(string));
        writer.WriteStartElement("test2");
        s2.Serialize(writer, test2);
        writer.WriteEndElement();
    }
}

public class Lesson3 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 IXmlSerializable是什么
        //C# 的XmlSerializer 提供了可拓展内容 
        //可以让一些不能被序列化和反序列化的特殊类能被处理
        //让特殊类继承 IXmlSerializable 接口 实现其中的方法即可
        #endregion

        #region 知识点二 自定义类实践
        TestLesson3 t = new TestLesson3();
        t.test2 = "123";
        string path = Application.persistentDataPath + "/TestLesson3.xml";
        //序列化
        using (StreamWriter writer = new StreamWriter(path))
        {
            //序列化"翻译机器"
            XmlSerializer s = new XmlSerializer(typeof(TestLesson3));
            //在序列化时  如果对象中的引用成员 为空 那么xml里面是看不到该字段的
            s.Serialize(writer, t);
        }
        //反序列化
        using (StreamReader reader = new StreamReader(path))
        {
            //序列化"翻译机器"
            XmlSerializer s = new XmlSerializer(typeof(TestLesson3));
            TestLesson3 t2 = s.Deserialize(reader) as TestLesson3;
        }
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

扩展Dictionary支持序列号反序列化

using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using UnityEngine;

public class SerizlizerDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    //自定义字典的 反序列化 规则
    public void ReadXml(XmlReader reader)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        //要跳过根节点
        reader.Read();
        //判断 当前不是元素节点 结束 就进行 反序列化
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            //反序列化键
            TKey key = (TKey)keySer.Deserialize(reader);
            //反序列化值
            TValue value = (TValue)valueSer.Deserialize(reader);
            //存储到字典中
            this.Add(key, value);
        }
    }

    //自定义 字典的 序列化 规则
    public void WriteXml(XmlWriter writer)
    {
        XmlSerializer keySer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSer = new XmlSerializer(typeof(TValue));

        foreach (KeyValuePair<TKey, TValue> kv in this)
        {
            //键值对 的序列化
            keySer.Serialize(writer, kv.Key);
            valueSer.Serialize(writer, kv.Value);
        }
    }
}

封装

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;

public class XmlDataMgr
{
    private static XmlDataMgr instance = new XmlDataMgr();

    public static XmlDataMgr Instance => instance;

    private XmlDataMgr() { }

    /// <summary>
    /// 保存数据到xml文件中
    /// </summary>
    /// <param name="data">数据对象</param>
    /// <param name="fileName">文件名</param>
    public void SaveData(object data, string fileName)
    {
        //1.得到存储路径
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        //2.存储文件
        using(StreamWriter writer = new StreamWriter(path))
        {
            //3.序列化
            XmlSerializer s = new XmlSerializer(data.GetType());
            s.Serialize(writer, data);
        }
    }

    /// <summary>
    /// 从xml文件中读取内容 
    /// </summary>
    /// <param name="type">对象类型</param>
    /// <param name="fileName">文件名</param>
    /// <returns></returns>
    public object LoadData(Type type, string fileName)
    {
        //1。首先要判断文件是否存在
        string path = Application.persistentDataPath + "/" + fileName + ".xml";
        if( !File.Exists(path) )
        {
            path = Application.streamingAssetsPath + "/" + fileName + ".xml";
            if (!File.Exists(path))
            {
                //如果根本不存在文件 两个路径都找过了
                //那么直接new 一个对象 返回给外部 无非 里面都是默认值
                return Activator.CreateInstance(type);
            }
        }
        //2.存在就读取
        using (StreamReader reader = new StreamReader(path))
        {
            //3.反序列化 取出数据
            XmlSerializer s = new XmlSerializer(type);
            return s.Deserialize(reader);
        }
    }
    
}

优缺点

优点

  • XML是国际通用的规则
  • 跨平台(游戏、软件、网页等等都可以用)
  • 文件结构浅显易懂,非常容易编辑和理解
  • 可以用于网络通讯进行交换数据

缺点

  • 重复工作繁多,且代码相似度极高
  • 自定义数据类型,都需要自己去实现存储和读取的功能
  • 数据容易被修改,只要找到位置就可以轻易修改数据

主要用处

网络游戏

  • 可以用于存储一些简单的不重要的数据
  • 可以用于传输信息(基本不会大范围使用,因为比较耗流量)

单机游戏

  • 用于存储游戏相关数据
  • 用于配置游戏数据

数据持久化之Json

Json是什么

        JSONJavaScript Object Notation  JS对象简谱 , 是一种轻量级的数据交换格式.

        主要在网络通讯中用于传输数据,或本地数据存储和读取(在游戏中可以把游戏数据按照Json格式标准存储在Json文档中,再将Json文档存储在硬盘上或者传输给远端,达到数据持久化或者数据传输的目的)

        易于人阅读和编写,同时也易于机器解析和生成,并有效的提高网络传输效率

Json和Xml的异同

     共同点

  •                 都是纯文本
  •                 都有层级结构
  •                 都具有描述性

不同点

  •                 Json配置更简单
  •                 Json在某种情况下读写更快

JsonUnity

          JsonUnity是Unity自带的公共类

  •         将内存中对象序列化为Json格式的字符串
  •          将Json字符串反序列化为类对象

   在文件中存读字符串

       1.存储字符串到指定路径文件中
第一个参数 填写的是 存储的路径
第二个参数 填写的是 存储的字符串内容
注意:第一个参数 必须是存在的文件路径 如果没有对应文件夹 会报错

        File.WriteAllText(Application.persistentDataPath + "/Test.json", "json文件");
        print(Application.persistentDataPath);

        2.在指定路径文件中读取字符串

        string str = File.ReadAllText(Application.persistentDataPath + "/Test.json");
        print(str);

JsonUnity序列化

序列化:把内存中的数据 存储到硬盘上

        方法:JsonUtility.ToJson(对象)

public class MrZhong
{
    public string name;
    public int age;
    public bool sex;
    public float testF;
    public double testD;

    public int[] ids;
    public List<int> ids2;
    public Dictionary<int, string> dic;
    public Dictionary<string, string> dic2;

    public Student s1;
    public List<Student> s2s;

    [SerializeField]
    private int privateI = 1;
    [SerializeField]
    protected int protectedI = 2;
}

 MrZhong t = new MrZhong();
 t.name = "Danny";
 t.age = 18;
 t.sex = false;
 t.testF = 1.4f;
 t.testD = 1.4;

 t.ids = new int[] { 1, 2, 3, 4 };
 t.ids2 = new List<int>() { 1, 2, 3 };
 t.dic = new Dictionary<int, string>() { { 1, "123" }, { 2, "234" } };
 t.dic2 = new Dictionary<string, string>() { { "1", "123" }, { "2", "234" } };

 t.s1 = null;//new Student(1, "小红");
 t.s2s = new List<Student>() { new Student(2, "小明"), new Student(3, "小强") };

 //Jsonutility提供了线程的方法 可以把类对象 序列化为 json字符串
 string jsonStr = JsonUtility.ToJson(t);
 File.WriteAllText(Application.persistentDataPath + "/MrZhong.json", jsonStr);

注意:

  •         float序列化时看起来会有一些误差
  •         自定义类需要加上序列化特性[System.Serializable]
  •         想要序列化私有变量 需要加上特性[SerializeField]
  •        JsonUtility不支持字典
  •        JsonUtlity存储null对象不会是null 而是默认值的数据

JsonUtlity进行反序列化

反序列化:把硬盘上的数据 读取到内存中

        方法: JsonUtility.FromJson(字符串)

//读取文件中的 Json字符串
jsonStr = File.ReadAllText(Application.persistentDataPath + "/MrZhong.json");
//使用Json字符串内容 转换成类对象
MrZhong t2 = JsonUtility.FromJson(jsonStr, typeof(MrZhong)) as MrZhong;
//推荐这种写法
MrZhong t3 = JsonUtility.FromJson<MrZhong>(jsonStr);

       注意:  如果Json中数据少了,读取到内存中类对象中时不会报错

      注意事项


//1.JsonUtlity无法直接读取数据集合
public class RoleData
{
    public List<RoleInfo> list;
}
[System.Serializable]
public class RoleInfo
{
    public int hp;
    public int speed;
    public int volume;
    public string resName;
    public int scale;
}

[System.Serializable]
public class RoleInfo
{
    public int hp;
    public int speed;
    public int volume;
    public string resName;
    public int scale;
}
jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/RoleInfo2.json");
print(jsonStr);
//List<RoleInfo> roleInfoList = JsonUtility.FromJson<List<RoleInfo>>(jsonStr);
RoleData data = JsonUtility.FromJson<RoleData>(jsonStr);

//2.文本编码格式需要时UTF-8 不然无法加载

总结

  1. 必备知识点 —— File存读字符串的方法 ReadAllText和WriteAllText
  2. JsonUtlity提供的序列化反序列化方法 ToJson 和 FromJson
  3. 自定义类需要加上序列化特性 [System.Serializable]
  4. 私有保护成员 需要加上 [SerializeField]
  5. JsonUtlity不支持字典
  6. JsonUtlity不能直接将数据反序列化为数据集合
  7. Json文档编码格式必须是UTF-8

LitJson

        LitJson它是一个第三方库,用于处理Json的序列化和反序列化

  •         LitJson是C#编写的,体积小、速度快、易于使用
  •         它可以很容易的嵌入到我们的代码中
  •         只需要将LitJson代码拷贝到工程中即可

        获取LitJson

  •         1.前往LitJson官网
  •        2.通过官网前往GitHub获取最新版本代码
  •         3.讲代码拷贝到Unity工程中 即可开始使用LitJson

使用LitJson进行序列化

        方法: JsonMapper.ToJson(对象)

Danny t = new Danny();
t.name = "Danny";
t.age = 18;
t.sex = true;
t.testF = 1.4f;
t.testD = 1.4;

t.ids = new int[] { 1, 2, 3, 4 };
t.ids2 = new List<int>() { 1, 2, 3 };
//t.dic = new Dictionary<int, string>() { { 1, "123" }, { 2, "234" } };
t.dic2 = new Dictionary<string, string>() { { "1", "123" }, { "2", "234" } };

t.s1 = null;//new Student(1, "小红");
t.s2s = new List<Student2>() { new Student2(2, "小明"), new Student2(3, "小强") };

string jsonStr = JsonMapper.ToJson(t);
print(Application.persistentDataPath);
File.WriteAllText(Application.persistentDataPath + "/Danny.json", jsonStr);

注意:

  •         相对JsonUtlity不需要加特性
  •         不能序列化私有变量
  •         支持字典类型,字典的键 建议都是字符串 因为 Json的特点 Json中的键会加上双引号
  •         需要引用LitJson命名空间
  •         LitJson可以准确的保存null类型

使用LitJson反序列化

方法: JsonMapper.ToObject(字符串)

jsonStr = File.ReadAllText(Application.persistentDataPath + "/Danny.json");
//JsonData是LitJson提供的类对象 可以用键值对的形式去访问其中的内容
JsonData data = JsonMapper.ToObject(jsonStr);
print(data["name"]);
print(data["age"]);
//通过泛型转换 更加的方便 建议使用这种方式
Danny t = JsonMapper.ToObject<Danny>(jsonStr);

       注意:

  •         类结构需要无参构造函数,否则反序列化时报错
  •         字典虽然支持 但是键在使用为数值时会有问题 需要使用字符串类型

注意事项

//1.LitJson可以直接读取数据集合
jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/RoleInfo.json");
RoleInfo2[] arr = JsonMapper.ToObject<RoleInfo2[]>(jsonStr);

List<RoleInfo2> list = JsonMapper.ToObject<List<RoleInfo2>>(jsonStr);

jsonStr = File.ReadAllText(Application.streamingAssetsPath + "/Dic.json");
Dictionary<string, int> dicTest = JsonMapper.ToObject<Dictionary<string, int>>(jsonStr);

//2.文本编码格式需要是UTF-8 不然无法加载

总结

  •         LitJson提供的序列化反序列化方法 JsonMapper.ToJson和ToObject<>
  •         LitJson无需加特性
  •         LitJson不支持私有变量
  •         LitJson支持字典序列化反序列化
  •         LitJson可以直接将数据反序列化为数据集合
  •         LitJson反序列化时 自定义类型需要无参构造
  •         Json文档编码格式必须是UTF-8

封装

using LitJson;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

/// <summary>
/// 序列化和反序列化Json时  使用的是哪种方案
/// </summary>
public enum JsonType
{
    JsonUtlity,
    LitJson,
}

/// <summary>
/// Json数据管理类 主要用于进行 Json的序列化存储到硬盘 和 反序列化从硬盘中读取到内存中
/// </summary>
public class JsonMgr
{
    private static JsonMgr instance = new JsonMgr();
    public static JsonMgr Instance => instance;

    private JsonMgr() { }

    //存储Json数据 序列化
    public void SaveData(object data, string fileName, JsonType type = JsonType.LitJson)
    {
        //确定存储路径
        string path = Application.persistentDataPath + "/" + fileName + ".json";
        //序列化 得到Json字符串
        string jsonStr = "";
        switch (type)
        {
            case JsonType.JsonUtlity:
                jsonStr = JsonUtility.ToJson(data);
                break;
            case JsonType.LitJson:
                jsonStr = JsonMapper.ToJson(data);
                break;
        }
        //把序列化的Json字符串 存储到指定路径的文件中
        File.WriteAllText(path, jsonStr);
    }

    //读取指定文件中的 Json数据 反序列化
    public T LoadData<T>(string fileName, JsonType type = JsonType.LitJson) where T : new()
    {
        //确定从哪个路径读取
        //首先先判断 默认数据文件夹中是否有我们想要的数据 如果有 就从中获取
        string path = Application.streamingAssetsPath + "/" + fileName + ".json";
        //先判断 是否存在这个文件
        //如果不存在默认文件 就从 读写文件夹中去寻找
        if(!File.Exists(path))
            path = Application.persistentDataPath + "/" + fileName + ".json";
        //如果读写文件夹中都还没有 那就返回一个默认对象
        if (!File.Exists(path))
            return new T();

        //进行反序列化
        string jsonStr = File.ReadAllText(path);
        //数据对象
        T data = default(T);
        switch (type)
        {
            case JsonType.JsonUtlity:
                data = JsonUtility.FromJson<T>(jsonStr);
                break;
            case JsonType.LitJson:
                data = JsonMapper.ToObject<T>(jsonStr);
                break;
        }

        //把对象返回出去
        return data;
    }
}


数据持久化之2进制

        2进制是什么

        2进制是计算机技术中广泛采用的一种数制。2进制数据是用0和1两个数码来表示的数。他的基数是2,进位规则是“逢二进一”。

        计算机中存储的数据本质上都是2进制数的存储,在计算机中位(bit)是最小的存储单位。1位就是一个0或者一个1。

        也就是说一个文件的数据本质上都是由n个0和1组合而成的,通过不同的解析规则最终呈现在我们眼前。

    数据持久化之2进制的好处

        通过2进制进行数据持久化的好处:

  •         安全性较高
  •         效率较高
  •         为网络通信做铺垫

各类型数据转字节数据

      回顾C#知识——不同变量类型

  • 有符号 sbyte int short long
  • 无符号 byte uint ushort ulong
  • 浮点 float double decimal
  • 特殊 bool char string

****  回顾C#知识——变量的本质

                变量的本质是2进制
在内存中都以字节的形式存储着
1byte = 8bit
1bit(位)不是0就是1
通过sizeof方法可以看到常用变量类型占用的字节空间长度

print("有符号");
print("sbyte" + sizeof(sbyte) + "字节");//1字节
print("int" + sizeof(int) + "字节");//4字节
print("short" + sizeof(short) + "字节");//2字节
print("long" + sizeof(long) + "字节");//8字节
print("无符号");
print("byte" + sizeof(byte) + "字节");//1字节
print("uint" + sizeof(uint) + "字节");//4字节
print("ushort" + sizeof(ushort) + "字节");//2字节
print("ulong" + sizeof(ulong) + "字节");//8字节
print("浮点");
print("float" + sizeof(float) + "字节");//4字节
print("double" + sizeof(double) + "字节");//8字节
print("decimal" + sizeof(decimal) + "字节");//16字节
print("特殊");
print("bool" + sizeof(bool) + "字节");//1字节
print("char" + sizeof(char) + "字节");//2字节

2进制文件读写的本质

  • 它就是通过将各类型变量转换为字节数组
  • 将字节数组直接存储到文件中
  • 一般人是看不懂存储的数据的
  • 不仅可以节约存储空间,提升效率
  • 还可以提升安全性
  • 而且在网络通信中我们直接传输的数据也是字节数据(2进制数据)

        数据和字节数据相互转换

****C#提供了一个公共类帮助我们进行转化
我们只需要记住API即可
类名:BitConverter
命名空间:using System

//1.将各类型转字节
byte[] bytes = BitConverter.GetBytes(256);

//2.字节数组转各类型
int i = BitConverter.ToInt32(bytes, 0);
print(i);

                标准编码格式

                编码是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。
为保证编码的正确性,编码要规范化、标准化,即需有标准的编码格式。
常见的编码格式有ASCII、ANSI、GBK、GB2312、UTF - 8、GB18030和UNICODE等。

  •  计算机中数据的本质就是2进制数据
  •  编码格式就是用对应的2进制数 对应不同的文字
  •  由于世界上有各种不同的语言,所有会有很多种不同的编码格式
  •  不同的编码格式 对应的规则是不同的
  •  如果在读取字符时采用了不统一的编码格式,可能会出现乱码
  •  游戏开发中常用编码格式 UTF-8
  •  中文相关编码格式 GBK

        在C#中有一个专门的编码格式类 来帮助我们将字符串和字节数组进行转换

类名:Encoding;需要引用命名空间:using System.Text;

//1.将字符串以指定编码格式转字节
byte[] bytes2 = Encoding.UTF8.GetBytes("Danny");

//2.字节数组以指定编码格式转字符串
string s = Encoding.UTF8.GetString(bytes2);
print(s);

总结:我们可以通过BitConverter和Encoding类,将所有C#提供给我们的数据类型和字节数组之间进行相互转换了

文件操作相关

        代码中的文件操作是做什么

                在电脑上我们可以在操作系统中创建删除修改文件,可以增删查改各种各样的文件类型,代码中的文件操作就是通过代码来做这些事情

   文件相关操作公共类

                C#提供了一个名为File(文件)的公共类 ,让我们可以快捷的通过代码操作文件相关

                类名:File;命名空间: System.IO

   文件操作File类的常用内容

1.判断文件是否存在

if(File.Exists(Application.dataPath + "/UnityTeach.zhong"))
{
    print("文件存在");
}
else
{
    print("文件不存在");
}

                2.创建文件

FileStream fs = File.Create(Application.dataPath + "/UnityTeach.zhong");

                3.写入文件

//将指定字节数组 写入到指定路径的文件中
byte[] bytes = BitConverter.GetBytes(999);
File.WriteAllBytes(Application.dataPath + "/UnityTeach.zhong", bytes);

//将指定的string数组内容 一行行写入到指定路径中
string[] strs = new string[] { "123", "Danny", "123123kdjfsalk", "123123123125243"};
File.WriteAllLines(Application.dataPath + "/UnityTeach2.zhong", strs);

//将指定字符串写入指定路径
File.WriteAllText(Application.dataPath + "/UnityTeach3.zhong", "Danny666\n哈哈哈哈123123131231241234123");

                4.读取文件

//读取字节数据
bytes = File.ReadAllBytes(Application.dataPath + "/UnityTeach.zhong");
print(BitConverter.ToInt32(bytes, 0));

//读取所有行信息
strs = File.ReadAllLines(Application.dataPath + "/UnityTeach2.zhong");
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

//读取所有文本信息
print(File.ReadAllText(Application.dataPath + "/UnityTeach3.zhong"));

                5.删除文件

//注意 如果删除打开着的文件 会报错
File.Delete(Application.dataPath + "/UnityTeach.zhong");

                6.复制文件

//参数一:现有文件 需要是流关闭状态
//参数二:目标文件
File.Copy(Application.dataPath + "/UnityTeach2.zhong", Application.dataPath + "/Danny.tanglaoshi", true);

                7.文件替换

//参数一:用来替换的路径
//参数二:被替换的路径
//参数三:备份路径
File.Replace(Application.dataPath + "/UnityTeach3.zhong", Application.dataPath + "/Danny.tanglaoshi", Application.dataPath + "/Danny备份.tanglaoshi");

                8.以流的形式 打开文件并写入或读取

//参数一:路径
//参数二:打开模式
//参数三:访问模式
FileStream fs = File.Open(Application.dataPath + "/UnityTeach2.zhong", FileMode.OpenOrCreate, FileAccess.ReadWrite);

文件操作相关文件流

        什么是文件流     

                在C#中提供了一个文件流类 FileStream类,它主要作用是用于读写文件的细节,我们之前学过的File只能整体读写文件,而FileStream可以以读写字节的形式处理文件

                也就是说:文件里面存储的数据就像是一条数据流(数组或者列表),我们可以通过FileStream一部分一部分的读写数据流。比如我可以先存一个int(4个字节)再存一个bool(1个字节)再存一个string(n个字节),利用FileStream可以以流式逐个读写。

           FileStream文件流类常用方法

                类名:FileStream,引用命名空间:System.IO

            1.打开或创建指定文件

                        方法一:new FileStream
参数一:路径
参数二:打开模式
CreateNew:创建新文件 如果文件存在 则报错
Create:创建文件,如果文件存在 则覆盖
Open:打开文件,如果文件不存在 报错
OpenOrCreate:打开或者创建文件根据实际情况操作
Append:若存在文件,则打开并查找文件尾,或者创建一个新文件
Truncate:打开并清空文件内容
参数三:访问模式
参数四:共享权限
None 谢绝共享
Read 允许别的程序读取当前文件
Write 允许别的程序写入该文件
ReadWrite 允许别的程序读写该文件

FileStream fs = new FileStream(Application.dataPath + "/Lesson3.Danny", FileMode.Create, FileAccess.ReadWrite);

                        方法二:File.Create
参数一:路径
参数二:缓存大小
参数三:描述如何创建或覆盖该文件(不常用)
Asynchronous 可用于异步读写
DeleteOnClose 不在使用时,自动删除
Encrypted 加密
None 不应用其它选项
RandomAccess 随机访问文件
SequentialScan 从头到尾顺序访问文件
WriteThrough 通过中间缓存直接写入磁盘

FileStream fs2 = File.Create(Application.dataPath + "/Lesson3.Danny");

                        方法三:File.Open
参数一:路径
参数二:打开模式

FileStream fs3 = File.Open(Application.dataPath + "/Lesson3.Danny", FileMode.Open);

                2.重要属性和方法

FileStream fs = File.Open(Application.dataPath + "Lesson3.Danny", FileMode.OpenOrCreate);
//文本字节长度
print(fs.Length);

//是否可读
if( fs.CanRead )
{
    print("文件可读");
}

//是否可写
if( fs.CanWrite )
{
    print("文件可写");

}

//将字节写入文件 当写入后 一定执行一次
fs.Flush();

//关闭流 当文件读写完毕后 一定执行
fs.Close();

//缓存资源销毁回收
fs.Dispose();

                3.写入字节

print(Application.persistentDataPath);
using (FileStream fs = new FileStream(Application.persistentDataPath + "/Lesson3.Danny", FileMode.OpenOrCreate, FileAccess.Write))
{
    byte[] bytes = BitConverter.GetBytes(999);
    //方法:Write
    //参数一:写入的字节数组
    //参数二:数组中的开始索引
    //参数三:写入多少个字节
    fs.Write(bytes, 0, bytes.Length);

    //写入字符串时
    bytes = Encoding.UTF8.GetBytes("Danny哈哈哈哈");
    //先写入长度
    //int length = bytes.Length;
    fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
    //再写入字符串具体内容
    fs.Write(bytes, 0, bytes.Length);

    //避免数据丢失 一定写入后要执行的方法
    fs.Flush();
    //销毁缓存 释放资源
    fs.Dispose();
}

                4.读取字节

                        方法一:挨个读取字节数组

using (FileStream fs2 = File.Open(Application.persistentDataPath + "/Lesson3.Danny", FileMode.Open, FileAccess.Read))
{
    //读取第一个整形
    byte[] bytes2 = new byte[4];

    //参数一:用于存储读取的字节数组的容器
    //参数二:容器中开始的位置
    //参数三:读取多少个字节装入容器
    //返回值:当前流索引前进了几个位置
    int index = fs2.Read(bytes2, 0, 4);
    int i = BitConverter.ToInt32(bytes2, 0);
    print("取出来的第一个整数" + i);//999
    print("索引向前移动" + index + "个位置");

    //读取第二个字符串
    //读取字符串字节数组长度
    index = fs2.Read(bytes2, 0, 4);
    print("索引向前移动" + index + "个位置");
    int length = BitConverter.ToInt32(bytes2, 0);
    //要根据我们存储的字符串字节数组的长度 来声明一个新的字节数组 用来装载读取出来的数据
    bytes2 = new byte[length];
    index = fs2.Read(bytes2, 0, length);
    print("索引向前移动" + index + "个位置");
    //得到最终的字符串 打印出来
    print(Encoding.UTF8.GetString(bytes2));
    fs2.Dispose();
}

                方法二:一次性读取再挨个读取

using (FileStream fs3 = File.Open(Application.persistentDataPath + "/Lesson3.Danny", FileMode.Open, FileAccess.Read))
{
    //一开始就申明一个 和文件字节数组长度一样的容器
    byte[] bytes3 = new byte[fs3.Length];
    fs3.Read(bytes3, 0, (int)fs3.Length);
    fs3.Dispose();
    //读取整数
    print(BitConverter.ToInt32(bytes3, 0));
    //得去字符串字节数组的长度
    int length2 = BitConverter.ToInt32(bytes3, 4);
    //得到字符串
    print(Encoding.UTF8.GetString(bytes3, 8, length2));
}

        更加安全的使用文件流对象

                using关键字重要用法
using (申明一个引用对象)
{
使用对象
}
无论发生什么情况 当using语句块结束后 ,会自动调用该对象的销毁方法 避免忘记销毁或关闭流,using是一种更安全的使用方法

                强调:
目前我们对文件流进行操作 为了文件操作安全 都用using来进行处理最好

   总结

  •                 通过FIleStream读写时一定要注意
  •                 读的规则一定是要和写是一致的
  •                 我们存储数据的先后顺序是我们制定的规则
  •                 只要按照规则读写就能保证数据的正确性

文件夹相关操作

        C#提供给我们的文件夹操作公共类

                类名:Directory;命名空间:using System.IO

        1.判断文件夹是否存在

if( Directory.Exists(Application.dataPath + "/数据持久化四"))
{
    print("存在文件夹");
}
else
{
    print("文件夹不存在");
}

        2.创建文件夹

DirectoryInfo info = Directory.CreateDirectory(Application.dataPath + "/数据持久化四");

   3.删除文件夹

//参数一:路径
//参数二:是否删除非空目录,如果为true,将删除整个目录,如果是false,仅当该目录为空时才可删除
Directory.Delete(Application.dataPath + "/数据持久化四");

     4.查找文件夹和文件

//得到指定路径下所有文件夹名
string[] strs = Directory.GetDirectories(Application.dataPath);
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

//得到指定路径下所有文件名
strs = Directory.GetFiles(Application.dataPath);
for (int i = 0; i < strs.Length; i++)
{
    print(strs[i]);
}

     5.移动文件夹

//如果第二个参数所在的路径 已经存在了一个文件夹 那么会报错
//移动会把文件夹中的所有内容一起移到新的路径
Directory.Move(Application.dataPath + "/数据持久化四", Application.dataPath + "/123123123");

    DirectoryInfo和FileInfo

        DirectoryInfo目录信息类,我们可以通过它获取文件夹的更多信息,它主要出现在两个地方

       1.创建文件夹方法的返回值

DirectoryInfo dInfo = Directory.CreateDirectory(Application.dataPath + "/数据持久化123");
//全路径
print(dInfo.FullName);
//文件名
print(dInfo.Name);

       2.查找上级文件夹信息

DirectoryInfo  dInfo = Directory.GetParent(Application.dataPath + "/数据持久化123");
//全路径
print(dInfo.FullName);
//文件名
print(dInfo.Name);

//重要方法
//得到所有子文件夹的目录信息
DirectoryInfo[] dInfos = dInfo.GetDirectories();

//FileInfo文件信息类
//我们可以通过DirectoryInfo得到该文件下的所有文件信息
FileInfo[] fInfos = dInfo.GetFiles();
for (int i = 0; i < fInfos.Length; i++)
{
    print("**************");
    print(fInfos[i].Name);//文件名
    print(fInfos[i].FullName);//路径
    print(fInfos[i].Length);//字节长度
    print(fInfos[i].Extension);//后缀名
}

c#类对象的序列化

       序列化类对象第一步—申明类对象

                注意:如果要使用C#自带的序列化2进制方法,申明类时需要添加[System.Serializable]特性

         序列化类对象第二步—将对象进行2进制序列化

        方法一:使用内存流得到2进制字节数组                 主要用于得到字节数组 可以用于网络传输                 新知识点                         1.内存流对象   类名:MemoryStream    命名空间:System.IO                         2.2进制格式化对象   类名:BinaryFormatter    命名空间:System.Runtime.Serialization.Formatters.Binary、  通过其中的序列化方法即可进行序列化生成字节数组                         主要方法:序列化方法 Serialize

//测试用例
[System.Serializable]
public class Person
{
    public int age = 1;
    public string name = "Danny";
    public int[] ints = new int[] { 1, 2, 3, 4, 5 };
    public List<int> list = new List<int>() { 1, 2, 3, 4 };
    public Dictionary<int, string> dic = new Dictionary<int, string>() { { 1,"123"},{ 2,"1223"},{ 3,"435345" } };
    public StructTest st = new StructTest(2, "123");
    public ClssTest ct = new ClssTest();
}

[System.Serializable]
public struct StructTest
{
    public int i;
    public string s;

    public StructTest(int i, string s)
    {
        this.i = i;
        this.s = s;
    }
}

[System.Serializable]
public class ClssTest
{
    public int i = 1;
}

Person p = new Person();
using (MemoryStream ms = new MemoryStream())
{   //将对象通过格式化程序写进内存流对象中
    //2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //序列化对象 生成2进制字节数组 写入到内存流当中
    bf.Serialize(ms, p);
    //得到对象的2进制字节数组
    byte[] bytes = ms.GetBuffer();
    //存储字节
    File.WriteAllBytes(Application.dataPath + "/Lesson5.Danny", bytes);
    //关闭内存流
    ms.Close();
}

        方法二:使用文件流进行存储

                主要用于存储到文件中

using (FileStream fs = new FileStream(Application.dataPath + "/Lesson5_2.Danny", FileMode.OpenOrCreate, FileAccess.Write))
{
    //2进制格式化程序
    BinaryFormatter bf = new BinaryFormatter();
    //序列化对象 生成2进制字节数组 写入到内存流当中
    bf.Serialize(fs, p);
    fs.Flush();
    fs.Close();
}

c#类对象的反序列化

        反序列化之 反序列化文件中数据

        主要类    FileStream文件流类    BinaryFormatter 2进制格式化类

        主要方法    Deserizlize   通过文件流打开指定的2进制数据文件

using (FileStream fs = File.Open(Application.dataPath + "/Lesson5_2.Danny", FileMode.Open, FileAccess.Read))
{
    //申明一个 2进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Person p = bf.Deserialize(fs) as Person;

    fs.Close();
}

  反序列化之 反序列化网络传输过来的2进制数据

        主要类       MemoryStream内存流类      BinaryFormatter 2进制格式化类
主要方法       Deserizlize ,目前没有网络传输 我们还是直接从文件中获取

byte[] bytes = File.ReadAllBytes(Application.dataPath + "/Lesson5_2.Danny");
//申明内存流对象 一开始就把字节数组传输进去
using (MemoryStream ms = new MemoryStream(bytes))
{
    //申明一个 2进制格式化类
    BinaryFormatter bf = new BinaryFormatter();
    //反序列化
    Person p = bf.Deserialize(ms) as Person;

    ms.Close();
}

c#2进制数据加密

   何时加密?何时解密?

                当我们将类对象转换为2进制数据时进行加密

                当我们将2进制数据转换为类对象时进行解密

加密是否是100%安全?

                定记住加密只是提高破解门槛,没有100%保密的数据,通过各种尝试始终是可以破解加密规则的,只是时间问题,加密只能起到提升一定的安全性

常用加密算法

              MD5算法、SHA1算法、HMAC算法、AES、DES、3DES算法 等等等

                用简单的异或加密感受加密的作用

Person p = new Person();
byte key = 199;
using (MemoryStream ms = new MemoryStream())
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(ms, p);
    byte[] bytes = ms.GetBuffer();
    //异或加密
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] ^= key;
    }
    File.WriteAllBytes(Application.dataPath + "/Lesson7.Danny", bytes);
}

//解密
byte[] bytes2 = File.ReadAllBytes(Application.dataPath + "/Lesson7.Danny");
for (int i = 0; i < bytes2.Length; i++)
{
    bytes2[i] ^= key;
}
using (MemoryStream ms = new MemoryStream(bytes2))
{
    BinaryFormatter bf = new BinaryFormatter();
    Person p2 = bf.Deserialize(ms) as Person;
    ms.Close();
}

优点

                效率高

                节约空间

                安全性高

缺点

                可读性差

                无法从数据文件看懂数据或修改数据

主要用处

                网络游戏

                        可以用于存储客户端数据

                        可以用于传输信息

                单机游戏

                        用于储存游戏相关数据

                        用于配置游戏数据

Unity添加菜单栏按钮

        为编辑器菜单栏添加新的选项入口

        以通过Unity提供我们的MenuItem特性在菜单栏添加选项按钮

        特性名:MenuItem   命名空间:UnityEditor

  •         规则一:一定是静态方法
  •         规则二:我们这个菜单栏按钮 必须有至少一个子路径(斜杠) 不然会报错 它不支持只有一个菜单栏入口
  •         规则三:这个特性可以用在任意的类当中
[MenuItem("GameTool/Danny")]
private static void Danny()
{
    Debug.Log("测试测试");
    //刷新Project窗口内容
    //类名:AssetDatabase
    //命名空间:UnityEditor
    //方法:Refresh

    Directory.CreateDirectory(Application.dataPath + "/Danny");
    //刷新资源管理器
    AssetDatabase.Refresh();
    #endregion
}

        Editor文件夹可以放在项目的任何文件夹下,可以有多个,放在其中的内容,项目打包时不会被打包到项目中,一般编辑器相关代码都可以放在该文件夹中

Excel DLL包的导入

       Excel表本质上也是一堆数据,只不过它有自己的存储读取规则,如果我们想要通过代码读取它,那么必须知道它的存储规则

        官网是专门提供了对应的DLL文件用来解析Excel文件的

        Dll文件:库文件,你可以理解为它是许多代码的集合,将相关代码集合在库文件中可以方便迁移和使用,有了某个DLL文件,我们就可以使用其中已经写好的代码

        而Excel的DLL包就是官方已经把解析Excel表的相关类和方法写好了,方便用户直接使用

        导入官方提供的Excel相关DLL文件,将DLL文件放置在Editor文件夹下

Excel数据读取

    打开Excel表

**    主要知识点:**

  •         FileStream读取文件流
  •         IExcelDataReader类,从流中读取Excel数据
  •         DataSet 数据集合类 将Excel数据转存进其中方便读取
[MenuItem("GameTool/打开Excel表")]
private static void OpenExcel()
{
    using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read ))
    {
        //通过我们的文件流获取Excel数据
        IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
        //将excel表中的数据转换为DataSet数据类型 方便我们 获取其中的内容
        DataSet result = excelReader.AsDataSet();
        //得到Excel文件中的所有表信息
        for (int i = 0; i < result.Tables.Count; i++)
        {
            Debug.Log("表名:" + result.Tables[i].TableName);
            Debug.Log("行数:" + result.Tables[i].Rows.Count);
            Debug.Log("列数:" + result.Tables[i].Columns.Count);
        }
        fs.Close();
    }
}

        获取Excel表中单元格的信息

                主要知识点:

  •         FileStream读取文件流
  •         IExcelDataReader类,从流中读取Excel数据
  •         DataSet 数据集合类 将Excel数据转存进其中方便读取
  •         DataTable 数据表类 表示Excel文件中的一个表
  •         DataRow 数据行类 表示某张表中的一行数据
[MenuItem("GameTool/读取Excel里的具体信息")]
private static void ReadExcel()
{
    using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read))
    {
        IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
        DataSet result = excelReader.AsDataSet();

        for (int i = 0; i < result.Tables.Count; i++)
        {
            //得到其中一张表的具体数据
            DataTable table = result.Tables[i];
            //得到其中一行的数据
            //DataRow row = table.Rows[0];
            //得到行中某一列的信息
            //Debug.Log(row[1].ToString());
            DataRow row;
            for (int j = 0; j < table.Rows.Count; j++)
            {
                //得到每一行的信息
                row = table.Rows[j];
                Debug.Log("*********新的一行************");
                for (int k = 0; k < table.Columns.Count; k++)
                {
                    Debug.Log(row[k].ToString());
                }
            }
        }
        fs.Close();
    }
}

封装

        ExcelTool

using Excel;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;

public class ExcelTool
{
    /// <summary>
    /// excel文件存放的路径
    /// </summary>
    public static string EXCEL_PATH = Application.dataPath + "/ArtRes/Excel/";

    /// <summary>
    /// 数据结构类脚本存储位置路径
    /// </summary>
    public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/DataClass/";

    /// <summary>
    /// 容器类脚本存储位置路径
    /// </summary>
    public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/Container/";

    /// <summary>
    /// 真正内容开始的行号
    /// </summary>
    public static int BEGIN_INDEX = 4;

    [MenuItem("GameTool/GenerateExcel")]
    private static void GenerateExcelInfo()
    {
        //记在指定路径中的所有Excel文件 用于生成对应的3个文件
        DirectoryInfo dInfo = Directory.CreateDirectory(EXCEL_PATH);
        //得到指定路径中的所有文件信息 相当于就是得到所有的Excel表
        FileInfo[] files = dInfo.GetFiles();
        //数据表容器
        DataTableCollection tableConllection;
        for (int i = 0; i < files.Length; i++)
        {
            //如果不是excel文件就不要处理了
            if (files[i].Extension != ".xlsx" &&
                files[i].Extension != ".xls")
                continue;
            //打开一个Excel文件得到其中的所有表的数据
            using (FileStream fs = files[i].Open(FileMode.Open, FileAccess.Read))
            {
                IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
                tableConllection = excelReader.AsDataSet().Tables;
                fs.Close();
            }

            //遍历文件中的所有表的信息
            foreach (DataTable table in tableConllection)
            {
                //生成数据结构类
                GenerateExcelDataClass(table);
                //生成容器类
                GenerateExcelContainer(table);
                //生成2进制数据
                GenerateExcelBinary(table);
            }

        }
    }

    /// <summary>
    /// 生成Excel表对应的数据结构类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelDataClass(DataTable table)
    {
        //字段名行
        DataRow rowName = GetVariableNameRow(table);
        //字段类型行
        DataRow rowType = GetVariableTypeRow(table);

        //判断路径是否存在 没有的话 就创建文件夹
        if (!Directory.Exists(DATA_CLASS_PATH))
            Directory.CreateDirectory(DATA_CLASS_PATH);
        //如果我们要生成对应的数据结构类脚本 其实就是通过代码进行字符串拼接 然后存进文件就行了
        string str = "public class " + table.TableName + "\n{\n";

        //变量进行字符串拼接
        for (int i = 0; i < table.Columns.Count; i++)
        {
            str += "    public " + rowType[i].ToString() + " " + rowName[i].ToString() + ";\n";
        }

        str += "}";

        //把拼接好的字符串存到指定文件中去
        File.WriteAllText(DATA_CLASS_PATH + table.TableName + ".cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 生成Excel表对应的数据容器类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelContainer(DataTable table)
    {
        //得到主键索引
        int keyIndex = GetKeyIndex(table);
        //得到字段类型行
        DataRow rowType = GetVariableTypeRow(table);
        //没有路径创建路径
        if (!Directory.Exists(DATA_CONTAINER_PATH))
            Directory.CreateDirectory(DATA_CONTAINER_PATH);

        string str = "using System.Collections.Generic;\n";

        str += "public class " + table.TableName + "Container" + "\n{\n";

        str += "    ";
        str += "public Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">";
        str += "dataDic = new " + "Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">();\n";

        str += "}";

        File.WriteAllText(DATA_CONTAINER_PATH + table.TableName + "Container.cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }


    /// <summary>
    /// 生成excel2进制数据
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelBinary(DataTable table)
    {
        //没有路径创建路径
        if (!Directory.Exists(BinaryDataMgr.DATA_BINARY_PATH))
            Directory.CreateDirectory(BinaryDataMgr.DATA_BINARY_PATH);

        //创建一个2进制文件进行写入
        using (FileStream fs = new FileStream(BinaryDataMgr.DATA_BINARY_PATH + table.TableName + ".ExcelData", FileMode.OpenOrCreate, FileAccess.Write))
        {
            //存储具体的excel对应的2进制信息
            //1.先要存储我们需要写多少行的数据 方便我们读取
            //-4的原因是因为 前面4行是配置规则 并不是我们需要记录的数据内容
            fs.Write(BitConverter.GetBytes(table.Rows.Count - 4), 0, 4);
            //2.存储主键的变量名
            string keyName = GetVariableNameRow(table)[GetKeyIndex(table)].ToString();
            byte[] bytes = Encoding.UTF8.GetBytes(keyName);
            //存储字符串字节数组的长度
            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
            //存储字符串字节数组
            fs.Write(bytes, 0, bytes.Length);

            //遍历所有内容的行 进行2进制的写入
            DataRow row;
            //得到类型行 根据类型来决定应该如何写入数据
            DataRow rowType = GetVariableTypeRow(table);
            for (int i = BEGIN_INDEX; i < table.Rows.Count; i++)
            {
                //得到一行的数据
                row = table.Rows[i];
                for (int j = 0; j < table.Columns.Count; j++)
                {
                    switch (rowType[j].ToString())
                    {
                        case "int":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "float":
                            fs.Write(BitConverter.GetBytes(float.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "bool":
                            fs.Write(BitConverter.GetBytes(bool.Parse(row[j].ToString())), 0, 1);
                            break;
                        case "string":
                            bytes = Encoding.UTF8.GetBytes(row[j].ToString());
                            //写入字符串字节数组的长度
                            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
                            //写入字符串字节数组
                            fs.Write(bytes, 0, bytes.Length);
                            break;
                    }
                }
            }

            fs.Close();
        }

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 获取变量名所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableNameRow(DataTable table)
    {
        return table.Rows[0];
    }

    /// <summary>
    /// 获取变量类型所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableTypeRow(DataTable table)
    {
        return table.Rows[1];
    }

    
    /// <summary>
    /// 获取主键索引
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static int GetKeyIndex(DataTable table)
    {
        DataRow row = table.Rows[2];
        for (int i = 0; i < table.Columns.Count; i++)
        {
            if (row[i].ToString() == "key")
                return i;
        }
        return 0;
    }
}

        BinaryDataMgr

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;

/// <summary>
/// 2进制数据管理器
/// </summary>
public class BinaryDataMgr
{
    /// <summary>
    /// 2进制数据存储位置路径
    /// </summary>
    public static string DATA_BINARY_PATH = Application.streamingAssetsPath + "/Binary/";

    /// <summary>
    /// 用于存储所有Excel表数据的容器
    /// </summary>
    private Dictionary<string, object> tableDic = new Dictionary<string, object>();

    /// <summary>
    /// 数据存储的位置
    /// </summary>
    private static string SAVE_PATH = Application.persistentDataPath + "/Data/";

    private static BinaryDataMgr instance = new BinaryDataMgr();
    public static BinaryDataMgr Instance => instance;

    private BinaryDataMgr()
    {

    }

    public void InitData()
    {
        LoadTable<PlayerInfoContainer, PlayerInfo>();
    }

    /// <summary>
    /// 加载Excel表的2进制数据到内存中 
    /// </summary>
    /// <typeparam name="T">容器类名</typeparam>
    /// <typeparam name="K">数据结构类类名</typeparam>
    public void LoadTable<T,K>()
    {
        //读取 excel表对应的2进制文件 来进行解析
        using (FileStream fs = File.Open(DATA_BINARY_PATH + typeof(K).Name + ".ExcelData", FileMode.Open, FileAccess.Read))
        {
            byte[] bytes = new byte[fs.Length];
            fs.Read(bytes, 0, bytes.Length);
            fs.Close();
            //用于记录当前读取了多少字节了
            int index = 0;

            //读取多少行数据
            int count = BitConverter.ToInt32(bytes, index);
            index += 4;

            //读取主键的名字
            int keyNameLength = BitConverter.ToInt32(bytes, index);
            index += 4;
            string keyName = Encoding.UTF8.GetString(bytes, index, keyNameLength);
            index += keyNameLength;

            //创建容器类对象
            Type contaninerType = typeof(T);
            object contaninerObj = Activator.CreateInstance(contaninerType);
            //得到数据结构类的Type
            Type classType = typeof(K);
            //通过反射 得到数据结构类 所有字段的信息
            FieldInfo[] infos = classType.GetFields();

            //读取每一行的信息
            for (int i = 0; i < count; i++)
            {
                //实例化一个数据结构类 对象
                object dataObj = Activator.CreateInstance(classType);
                foreach (FieldInfo info in infos)
                {
                    if( info.FieldType == typeof(int) )
                    {
                        //相当于就是把2进制数据转为int 然后赋值给了对应的字段
                        info.SetValue(dataObj, BitConverter.ToInt32(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(float))
                    {
                        info.SetValue(dataObj, BitConverter.ToSingle(bytes, index));
                        index += 4;
                    }
                    else if (info.FieldType == typeof(bool))
                    {
                        info.SetValue(dataObj, BitConverter.ToBoolean(bytes, index));
                        index += 1;
                    }
                    else if (info.FieldType == typeof(string))
                    {
                        //读取字符串字节数组的长度
                        int length = BitConverter.ToInt32(bytes, index);
                        index += 4;
                        info.SetValue(dataObj, Encoding.UTF8.GetString(bytes, index, length));
                        index += length;
                    }
                }

                //读取完一行的数据了 应该把这个数据添加到容器对象中
                //得到容器对象中的 字典对象
                object dicObject = contaninerType.GetField("dataDic").GetValue(contaninerObj);
                //通过字典对象得到其中的 Add方法
                MethodInfo mInfo = dicObject.GetType().GetMethod("Add");
                //得到数据结构类对象中 指定主键字段的值
                object keyValue = classType.GetField(keyName).GetValue(dataObj);
                mInfo.Invoke(dicObject, new object[] { keyValue, dataObj });
            }

            //把读取完的表记录下来
            tableDic.Add(typeof(T).Name, contaninerObj);

            fs.Close();
        }
    }

    /// <summary>
    /// 得到一张表的信息
    /// </summary>
    /// <typeparam name="T">容器类名</typeparam>
    /// <returns></returns>
    public T GetTable<T>() where T:class
    {
        string tableName = typeof(T).Name;
        if (tableDic.ContainsKey(tableName))
            return tableDic[tableName] as T;
        return null;
    }

    /// <summary>
    /// 存储类对象数据
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="fileName"></param>
    public void Save(object obj, string fileName)
    {
        //先判断路径文件夹有没有
        if (!Directory.Exists(SAVE_PATH))
            Directory.CreateDirectory(SAVE_PATH);

        using (FileStream fs = new FileStream(SAVE_PATH + fileName + ".ExcelData", FileMode.OpenOrCreate, FileAccess.Write))
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(fs, obj);
            fs.Close();
        }
    }

    /// <summary>
    /// 读取2进制数据转换成对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="fileName"></param>
    /// <returns></returns>
    public T Load<T>(string fileName) where T:class
    {
        //如果不存在这个文件 就直接返回泛型对象的默认值
        if( !File.Exists(SAVE_PATH + fileName + ".ExcelData") )
            return default(T);

        T obj;
        using (FileStream fs = File.Open(SAVE_PATH + fileName + ".ExcelData", FileMode.Open, FileAccess.Read))
        {
            BinaryFormatter bf = new BinaryFormatter();
            obj = bf.Deserialize(fs) as T;
            fs.Close();
        }

        return obj;
    }
}

结尾

以上是本人在项目中及平时积累的知识归纳总结!!!

如有疑问或BUG,请加入QQ群一起交流探讨!!!

技术交流QQ群:1011399921!!!