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类可以对任意类型的文件进行读取操作,而且我们也可以根据自己需要来指定每一次读取字节长度,以此减少内存的消耗,提高读取效率。
直观点: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类帮我们实现在流上读写字符串的功能。
-
BinaryReader 和 BinaryWriter 这两个类提供了从字符串或原始数据到各种流之间的读写操作。
-
TextReader 和 TextWriter 类都是抽象类。和Stream类的字节形式的输入和输出不同,它们用于Unicode字符的输入和输出。
-
StringReader 和 StringWriter 在字符串中读写字符。
-
StreamReader 和 StreamWriter 在流中读写字符。
三、注意实现
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");
}
}
}