zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

Unity 基础 之 实现简单的Android移动端本地数据读取与写入封装(简单加密写入,解密读取 json 数据)

2023-09-11 14:20:50 时间

 

 

Unity 基础 之 实现简单的Android移动端本地数据读取与写入封装(简单加密写入,解密读取 json 数据)

 

目录

Unity 基础 之 实现简单的Android移动端本地数据读取与写入封装(简单加密写入,解密读取 json 数据)

一、简单介绍

二、相关知识说明

三、注意实现

四、效果预览(演示为UnityEditor,测试过移动端同理可行)

五、实现步骤

六、关键代码


 

一、简单介绍

Unity中的一些基础知识点。

本节介绍,在 Unity 中,简单实现在移动端,读取与写入数据到本地,这以写入json数据为例,同时在写入和读取中,进行简单的数据加密和解密操作,便于后期使用,有不对,欢迎指正。

 

       Application.dataPath

  返回程序的数据文件所在文件夹的路径。例如在Editor中就是Assets了。  

  Application.streamingAssetsPath

  返回流数据的缓存目录,返回路径为相对路径,适合设置一些外部数据文件的路径。会随包导出。

  1. 只读不可写。

  2. 主要用来存放二进制文件。

  3. 只能用过WWW类来读取。

  Application.persistentDataPath

  返回一个持久化数据存储目录的路径,可以在此路径下存储一些持久化的数据文件。例如Assetbundle等资源

  1. 内容可读写,不过只能运行时才能写入或者读取。提前将数据存入这个路径是不可行的。

  2. 无内容限制。可以从StreamingAsset中读取二进制文件或者从AssetBundle读取文件来写入PersistentDataPath中。

 

二、相关知识说明

1、什么是FileStream类

  FileStream 类对文件系统上的文件进行读取、写入、打开和关闭操作,并对其他与文件相关的操作系统句柄进行操作,如管道、标准输入和标准输出。读写操作可以指定为同步或异步操作。FileStream 对输入输出进行缓冲,从而提高性能。——MSDN

  简单点说:FileStream类可以对任意类型的文件进行读取操作,而且我们也可以根据自己需要来指定每一次读取字节长度,以此减少内存的消耗,提高读取效率。

2、File和FileStream的区别

  直观点:File是一个静态类;FileStream是一个非静态类。

  File:是一个文件的类,对文件进行操作。其内部封装了对文件的各种操作(MSDN:提供用于创建、复制、删除、移动和打开单一文件的静态方法,并协助创建FileStream对象)。

  FileStream: 文件流的类。对txt,xml,avi等文件进行内容写入、读取、复制...时候需要使用的一个工具。

  打个形象的比喻。File是笔记本,需要Filestream的这个笔才能写.

  换而言之,记事本是一个文件,可以用File操作,里面的内容需要用FileStream来操作。

3、FileStream代码示例:

一、创建一个文件流
FileStream fs = new FileStream(@"c:\中国.txt", FileMode.Create, FileAccess.Write);

string txt = "中国是世界上人口第一大国。中国是世界上最幸福的国家之一。";
byte[] buffer = Encoding.UTF8.GetBytes(txt);

二、读文件或者写文件
//参数1:表示要把哪个byte[]数组中的内容写入到文件
//参数2:表示要从该byte[]数组的第几个下标开始写入,一般都是0
//参数3:要写入的字节的个数。
fs.Write(buffer, 0, buffer.Length);
//fs.Read(buffer, 0, buffer.Length);

三、关闭文件流
fs.flush();仅仅是清空缓冲区,流对象还可以继续使用。
fs.close();关闭流对象,但是先刷新一次缓冲区,关闭之后,流对象不可以继续再使用了。
fs.Dispose();//自动调用close和flush方法


简洁的写法

//一、创建一个文件流
//当把一个对象放到using()中的时候,当超出using的作用于范围后,会自动调用该对象的Dispose()方法。
using (FileStream fs = new FileStream(@"c:\中国.txt", FileMode.Create, FileAccess.Write))
{
    string txt = "中国是世界上人口第一大国。中国是世界上最幸福的国家之一。";
    byte[] buffer = Encoding.UTF8.GetBytes(txt);
    fs.Write(buffer, 0, buffer.Length);
}

 

4、Stream 流

Stream在msdn的定义:为字节序列提供通用的操作视图,这个解释太抽象了,不容易理解;从stream的字面意思“河,水流”更容易理解些,
Stream是一个抽象类,它定义了类似“水流”的事物的一些统一行为,包括这个“水流”是否可以抽水出来(读取流内容);是否可以往这个“水流”中注水(向流中写入内容);以及这个“水流”有多长;如何关闭“水流”,如何向“水流”中注水,如何从“水流”中抽水等“水流”共有的行为。

5、Stream的子类

  • 1.MemoryStream 存储在内存中的字节流。

  • 2.FileStream 存储在文件系统的字节流。

  • 3.NetworkStream 通过网络设备读写的字节流。

  • 4.BufferedStream 为其他流提供缓冲的流。

区别

  • BufferedStream 是为诸如网络 流的其它流添加缓冲的一种流类型。其实,FileStream流自身内部含有缓冲,而MemorySteam流则不需要缓冲。一个BufferStream 类的实例可以由多个其它类型的流复合而成,以达到提高性能的目的。缓冲实际上是内存中的一个字节块,利用缓冲可以避免操作系统频繁地到磁盘上读取数据,从而减轻了操作系统的负担。

  • MemoryStream 是一个无缓冲流,它所封装的数据直接放在内存中,因此可以用于快速临时存储、进程间传递信息等。
    这两个类都是缓冲区,都实现了对内存进行数据读写的功能,而不是对持久性存储器进行读写。
    但BufferedStream必须跟其他流如FileStream结合使用,而MemoryStream则不用。

  • Networksteam 表示在互联网络上传递的流。

  • FileStream 表示本地文件的读取和写入流

6、Stream的常用方法

属性/方法

用法

Length

获取用字节表示的流长度。

Position

获取或设置此流的当前位置。

Read(Byte[], Int32, Int32)

从流中读取字节块并将该数据写入给定缓冲区中。

ReadAsync(Byte[], Int32, Int32)

从当前流异步读取字节序列,并将流中的位置提升读取的字节数。

ReadByte()

从文件中读取一个字节,并将读取位置提升一个字节。

Seek(Int64, SeekOrigin)

将该流的当前位置设置为给定值。

Write(Byte[], Int32, Int32)

将字节块写入文件流。

WriteAsync(Byte[], Int32, Int32)

将字节序列异步写入当前流,并将流的当前位置提升写入的字节数。

WriteByte(Byte)

一个字节写入文件流中的当前位置。

Flush()

清除此流的缓冲区,使得所有缓冲数据都写入到文件中。

7、Reader And Write

Stream提供了读写流的方法是以字节的形式从流中读取内容。而我们经常会用到从字节流中读取文本或者写入文本,微软提供了StreamReader和StreamWriter类帮我们实现在流上读写字符串的功能。

  • BinaryReaderBinaryWriter 这两个类提供了从字符串或原始数据到各种流之间的读写操作。

  • TextReaderTextWriter 类都是抽象类。和Stream类的字节形式的输入和输出不同,它们用于Unicode字符的输入和输出。

  • StringReaderStringWriter 在字符串中读写字符。

  • StreamReaderStreamWriter 在流中读写字符。

 

三、注意实现

1、在没有读取数据的时候,这里临时读取网络接口的数据

2、这里的数据加密使用的是 Base64Code 的方法

3、Android 端读写入数据的位置是:Application.persistentDataPath(较适合移动端操作)

4、这里设计数据的 json 转换,使用的是 litjson 包,进行数据转换

 

四、效果预览(演示为UnityEditor,测试过移动端同理可行)

 

五、实现步骤

1、打开Unity,新建空工程

 

2、在场景中布局UI,测试数据的读取写入,和数据展示

 

3、导入 litjson,并且编写脚本,AndroidReadWriteDataWrapper 数据的读取和写入接口,以及可能用到的数据加密解密代码,MonoSingleton 单例类,TestAndroidReadWriteDataWrapper 测试 AndroidReadWriteDataWrapper  功能

 

4、把 TestAndroidReadWriteDataWrapper  挂载到场景中,并对应赋值

 

5、运行场景,打包到Android 设备上运行,效果如上

 

6、保存的加密数据,如下

 

六、关键代码

1、AndroidReadWriteDataWrapper

using LitJson;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

namespace XANTools
{
    public class AndroidReadWriteDataWrapper : MonoSingleton<AndroidReadWriteDataWrapper>
    {
        // 新闻数据的地址(本地没有数据的时候去网上取数据,根据需要设定的场景)
        string newsDataUrl = "https://launcher.madgaze.dev/v1/news/newsFeedList/cn";

        // 文件路径 (根据需要)
        string filePath;

        // 本地化保存是否加密标志信息
        const string PlayerPrefsName = "AndroidReadWriteDataWrapper" + "_" + "IsEncryption";
        private void Start()
        {
#if UNITY_EDITOR
            filePath = Application.dataPath + "/TestInfoFile.json";
#else
            filePath = Application.persistentDataPath + "/" + "TestInfoFile.jjson";
#endif
        }

        // 新闻数据字段
        NewsDataStructFromAPI _newsDataStructFromAPI;
        List<NewsDataStruct> _newsDataStructs;

        // 天气获取成功的事件
        Action<List<NewsDataStruct>> GetNewsDataSucessAction;
        Action<string> GetNewsDataFailAction;

        // 新闻数据属性
        public NewsDataStructFromAPI NewsDataStructFromAPI { get => _newsDataStructFromAPI; private set => _newsDataStructFromAPI = value; }
        public List<NewsDataStruct> NewsDataStructs { get => _newsDataStructs; private set => _newsDataStructs = value; }

        /// <summary>
        /// 读取数据
        /// </summary>
        /// <param name="getNewsDataSucessHandler"></param>
        /// <param name="getNewsDataFailHandler"></param>
        public void Read(Action<List<NewsDataStruct>> getNewsDataSucessHandler, Action<string> getNewsDataFailHandler) {

            Read(filePath,getNewsDataSucessHandler,getNewsDataFailHandler);

        }

        /// <summary>
        /// 读取数据
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public void Read(string path, Action<List<NewsDataStruct>> getNewsDataSucessHandler, Action<string> getNewsDataFailHandler)
        {

            GetNewsDataSucessAction = getNewsDataSucessHandler;
            GetNewsDataFailAction = getNewsDataFailHandler;

            // 文件存在与否
            if (File.Exists(path) == false)
            {
                StartCoroutine(GetRequest(newsDataUrl));
            }
            else
            {
                StartCoroutine(ReadFile(path));
            }


        }

        /// <summary>
        /// 写入数据
        /// </summary>
        /// <param name="newsDataStructs"></param>
        public void Write(List<NewsDataStruct> newsDataStructs) {
            Write(filePath,newsDataStructs);
        }

        /// <summary>
        /// 写入数据
        /// </summary>
        /// <param name="path"></param>
        /// <param name="newsDataStructs"></param>
        public void Write(string path, List<NewsDataStruct> newsDataStructs, bool isEncryption = false) {
            StartCoroutine(WriteFile(path, newsDataStructs, isEncryption));
        }

        /// <summary>
        /// 获取新闻数据
        /// </summary>
        /// <returns></returns>
        public List<NewsDataStruct> GetNewsData() {

            return NewsDataStructs;
        }

        #region 内部私有方法

        /// <summary>
        /// 获取网络数据
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        IEnumerator GetRequest(string url)
        {

            using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
            {
                Debug.Log(GetType() + "/UnityWebRequest.Get()/");

                yield return webRequest.SendWebRequest();

                // 可以放在 Update 中显示 
                //Debug.Log("webRequest.uploadProgress " + webRequest.uploadProgress);

                if (webRequest.isHttpError || webRequest.isNetworkError)
                {
                    Debug.LogError(webRequest.error + "\n" + webRequest.downloadHandler.text);

                    // 执行回调
                    if (GetNewsDataFailAction != null)
                    {
                        GetNewsDataFailAction(webRequest.downloadHandler.text);
                    }
                }
                else
                {
                    string weatherJsonStr = webRequest.downloadHandler.text;
                    Debug.Log(GetType() + "/GetRequest()/ JsonStr : " + weatherJsonStr);


                    // 解析数据
                    NewsDataStructFromAPI = JsonMapper.ToObject<NewsDataStructFromAPI>(weatherJsonStr);
                    NewsDataStructs = NewsDataStructFromAPI.data;
                    if (NewsDataStructs == null)
                    {
                        Debug.Log(GetType() + "/GetRequest/ NewsDataStructs ==null");
                    }
                    else
                    {
                        Debug.Log(GetType() + "/GetRequest()/ NewsDataStructs[0].name :" + NewsDataStructs[0].name);

                    }


                    // 执行回调
                    if (GetNewsDataSucessAction != null)
                    {
                        GetNewsDataSucessAction(NewsDataStructs);
                    }
                }
            }



        }

        /// <summary>
        /// 读取数据
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        IEnumerator ReadFile(string path) {

            yield return new WaitForEndOfFrame();
            try
            {
                FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
                StreamReader sr = new StreamReader(fs, Encoding.UTF8);
                string data = sr.ReadToEnd();

                // 加密的数据,需要解密
                bool IsEncryption = (PlayerPrefs.GetInt(PlayerPrefsName,0)==1)?true:false;
                if (IsEncryption == true)
                {
                    data = Base64CodeDecryption(data);
                }

                NewsDataStructs = JsonMapper.ToObject<List<NewsDataStruct>>(data);
                // 执行回调
                if (GetNewsDataSucessAction != null)
                {
                    GetNewsDataSucessAction(NewsDataStructs);
                }
                sr.Close();
                fs.Close();
            }
            catch (IOException e)
            {
                Debug.LogError(GetType() + "/ReadFile()/FileRead: " + e.Message);
            }
        }

        /// <summary>
        /// 写入文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="newsDataStructs"></param>
        /// <returns></returns>
        IEnumerator WriteFile(string path, List<NewsDataStruct> newsDataStructs, bool isEncryption) {
           
            // 本地化保存是否加密标志信息
            PlayerPrefs.SetInt(PlayerPrefsName,isEncryption?1:0);
            PlayerPrefs.Save();

            yield return new WaitForEndOfFrame();
            try
            {
                FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
                StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
                string data = JsonMapper.ToJson(newsDataStructs);

                // 加密数据
                if (isEncryption == true)
                {
                    data = Base64CodeEncryption(data);
                }

                sw.WriteLine(data);
                sw.Close();
                fs.Close();
            }
            catch (Exception ex)
            {
                Debug.LogError(GetType() + "/WriteFile()/Write File: " + ex.Message);
            }
        }



       
        /// <summary>
        /// Base64位对称加密
        /// </summary>
        /// <param name="data">要加密的数据</param>
        /// <returns></returns>
        string Base64CodeEncryption(string data)
        {         
            byte[] byteStr = System.Text.Encoding.UTF8.GetBytes(data);
            string str = Convert.ToBase64String(byteStr);//转换后的64位字符串
            Debug.Log(GetType()+ "/Base64CodeEncryption()/Base64加密后的数据:" + str);

            return str;
        }

        /// <summary>
        /// Base64位对称解密
        /// </summary>
        /// <param name="data">要解密的数据</param>
        /// <returns></returns>
        string Base64CodeDecryption(string data) {
            //解密
            byte[] temp = Convert.FromBase64String(data);
            string str = System.Text.Encoding.UTF8.GetString(temp);//64位恢复字符串
            Debug.Log(GetType() + "/Base64CodeDecryption()/Base64解密后的数据:" + str);

            return str;
        }

        #endregion 内部私有方法
    }

    public class NewsDataStructFromAPI
    {
        public int code;
        public string msg;
        public List<NewsDataStruct> data;

        public NewsDataStructFromAPI() { }
    }

    public class NewsDataStruct
    {
        public string name;
        public string url;
        public string icon;
        public bool IsLoved;

        public NewsDataStruct() { }

        public NewsDataStruct(string name, string url, string icon, bool isLoved)
        {
            this.name = name;
            this.url = url;
            this.icon = icon;
            IsLoved = isLoved;
        }

        public override string ToString()
        {
            return string.Format("Name:{0},Url:{1},IconUrl:{2},IsLoved:{3}", name, url, icon, IsLoved);
        }
    }
}

/* 数据格式
 {
	"code": 0,
	"msg": "success",
	"data": [{
		"name": "人民网",
		"url": "http://www.people.com.cn/",
		"icon": "http://file.madgaze.cn/assets/launcher/bbc-test.png"
	}, {
		"name": "凤凰网",
		"url": "http://news.ifeng.com/",
		"icon": "http://file.madgaze.cn/assets/launcher/bbc-test.png"
	}, {
		"name": "央视网",
		"url": "https://news.cctv.com/",
		"icon": "http://file.madgaze.cn/assets/launcher/bbc-test.png"
	}, {
		"name": "网易新闻",
		"url": "https://news.163.com/",
		"icon": "http://file.madgaze.cn/assets/launcher/bbc-test.png"
	}]
}
     
     */

 

2、MonoSingleton

using UnityEngine;

public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance = null;

    private static readonly object locker = new object();

    private static bool bAppQuitting;

    public static T Instance
    {
        get
        {
            if (bAppQuitting)
            {
                instance = null;
                return instance;
            }

            lock (locker)
            {
                if (instance == null)
                {
                    // 保证场景中只有一个 单例
                    T[] managers = Object.FindObjectsOfType(typeof(T)) as T[];
                    if (managers.Length != 0)
                    {
                        if (managers.Length == 1)
                        {
                            instance = managers[0];
                            instance.gameObject.name = typeof(T).Name;
                            return instance;
                        }
                        else
                        {
                            Debug.LogError("Class " + typeof(T).Name + " exists multiple times in violation of singleton pattern. Destroying all copies");
                            foreach (T manager in managers)
                            {
                                Destroy(manager.gameObject);
                            }
                        }
                    }


                    var singleton = new GameObject();
                    instance = singleton.AddComponent<T>();
                    singleton.name = "(singleton)" + typeof(T);
                    singleton.hideFlags = HideFlags.None;
                    DontDestroyOnLoad(singleton);

                }
                instance.hideFlags = HideFlags.None;
                return instance;
            }
        }
    }

    protected virtual void Awake()
    {
        bAppQuitting = false;

        
    }

    protected virtual void OnDestroy()
    {
        bAppQuitting = true;
    }
}

 

3、TestAndroidReadWriteDataWrapper

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using XANTools;

public class TestAndroidReadWriteDataWrapper : MonoBehaviour
{
    public Text Data_Text;
    public Button Read_Button;
    public Button Write_Button;

    string filePath ;

    private List<NewsDataStruct> _newsDataStructs;

    // Start is called before the first frame update
    void Start()
    {
#if UNITY_EDITOR
        filePath = Application.dataPath + "/TestInfoFile.json";
#else
        filePath = Application.persistentDataPath + "/" + "TestInfoFile.jjson";
#endif
        Read_Button.onClick.AddListener(Read);
        Write_Button.onClick.AddListener(Write);
    }

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

    void Read() {

        Debug.Log(GetType() + "/Read()");
        AndroidReadWriteDataWrapper.Instance.Read(filePath,(newsDatas)=> {
            Data_Text.text = "";
            _newsDataStructs = newsDatas;
            foreach (NewsDataStruct item in newsDatas)
            {
                Data_Text.text += item.ToString() + "\n";
            }

        },(failInfo)=> {
            Data_Text.text = failInfo;
        });
    }

    void Write() {
        Debug.Log(GetType() + "/Write()");
        if (_newsDataStructs != null)
        {
            AndroidReadWriteDataWrapper.Instance.Write(filePath, _newsDataStructs,true);

        }
        else {
            Debug.Log(GetType()+ "/Write()/_newsDataStructs == null");
        }
    }
}