👉一、背景
项目中有引用到外部图片资源的功能(由用户选择),而且当用户下次打开这个项目的时候也要显示用户上次使用过的图片,这就涉及到了一个图片数据的可持久化存储问题。由于项目中的其他数据是保存到XML文件中的,所以本博客仅此记录Unity实现在Xml文件中图片资源的序列化与反序列化问题的过程,并给出Demo验证。
👉二、解决思路及过程
- 我一开始的做法是用户选择一张图,通过代码将图片解析成二进制流,然后通过Texture类解析成纹理图,最后创建精灵图赋值到Image组件上。
(缺点:没有对图片进行缓存,每次选择图片都要重新读取并且实例化,可能会影响性能) - 创建图片管理类SpriteManager,定义字典spriteDic,key为图片名字,value为精灵图。用户第一次选择的时候,解析成图片保存到字典中,公开一个GetSprite方法供外部调用字典里的精灵图,选取图片时先判断字典是否存在同名的key,存在则直接返回sprite,否则就解析。
(优点:避免重复读取及实例化;缺点:保存项目后再打开图片不会显示(没有完成持久化存储)) - 又考虑到可以在图片管理类中增加一个字典,在xml文件中保存图片名和对应路径,这样虽然能实现下次打开项目时初始化将这些图片再解析出来并显示(前提是外部图片路径不会被修改),为了避免这种情况,所以放弃这个想法。
后来又想到一种可行方法:在项目文件夹下创建一个专门用来保存用户选择的图片,下次打开项目时先遍历这个图片文件夹,解析图片数据并保存。再次打开项目时就不会出现图片丢失的情况了。看上去功能像是实现了。(缺点:这样做的结果是将项目数据跟图片数据从物理上“分离”了,如果项目xml文件迁移了,而图片文件夹没有一起迁移,那么下次打开这个项目还是读取不到上次选择过的相关图片资源。而且图片越多,图片文件夹越大,虽然不占用包体,但不好管理。) - 于是又得重新思考别的方法。那么有什么方法既能实现需求,又能保证项目xml文件与图片资源“融为一体”呢?答案之一就是将图片保存到xml文件中。对,你没有看错,图片保存到xml中!具体思路就是:把用户选择的图片解析成字节流,除了需转为sprite供界面显示之外,还需将其字节流转为字符串的形式保存到xml文件中(序列化过程);下次打开项目xml时,读取保存的图片节点信息,将图片字符串数据转为字节流,再将字节流转为精灵图sprite显示到图片组件上(反序列化过程)。
刚开始是用户没选择一张图片就保存一张图片的字符串,考虑到可能会出现保存图片数据冗余的情况,将保存代码优化:如果多个图片组件使用用一张图片,那么xml中仅保存一份该图片的字符串数据。 - 后面又考虑到图片名字不能代表图片内容是一致的(同一个图片可能存在改名的情况),所以需要获取到图片的唯一标识符MD5作未图片数据的保存对象。
所以最终定下的解决方案是:封装图片数据到SpriteData类中,成员为Sprite(精灵图)、ImgStr(图片字符串)。在图片管理类中定义spriteDataDics字典来保存图片的md5值和SpriteData对象,序列化时遍历该字典保存到xml中,反序列时
解析xml数据保存到字典中,这样就保证了下次打开该项目文件还能显示相关使用过的图片。
👉三、原理
xml序列化:图片byte[]→string字符串→(写入)xml。
xml反序列化:xml→(读取)string字符串→byte[]→Texture/Sprite。
👉四、图片反序列与反序列化Demo验证
1、搭建验证demo
- demo场景由一个面板Panel、两个按钮Button(序列化和反序列化图片按钮)、滚动视图ScrollView和三个图片组件Image组成。
- demo中使用到了本地资源浏览器插件Crosstales.FileBrowser。该插件使用和获取教程:FileBrowser教程。使用该插件还需引用dll:System.Windows.Forms。
- 创建的脚本SpriteManager,挂载到Panel上,注意拖拽赋值,脚本代码如下文。
2、demo完整代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Xml;//解析xml引入的命名空间
using Crosstales.FB;//文件资源浏览器需引用的命名空间,需要插件
using System.Security.Cryptography;//获取图片文件md5值需引用的命名空间
//精灵图数据
public class SpriteData
{
public string ImgStr { get; set; }
public Sprite Sprite { get; set; }
public SpriteData(string imgStr,Sprite sprite)
{
this.ImgStr = imgStr;
this.Sprite = sprite;
}
}
//图片管理类
public class SpriteManager : MonoBehaviour
{
public Button serializeBtn;//序列化图片按钮
public Button deserializeBtn;//反序列化图片按钮
public Transform imgContent;//场景中图片组件的父物体
//缓存图片md5和精灵图数据的字典
private Dictionary<string,SpriteData> spriteDataDics = new Dictionary<string,SpriteData>();
//图片组件和赋值的图片md5做一个映射关系的字典(用来知道哪个物体赋值了哪张图)
private Dictionary<string, string> imgObjMaps = new Dictionary<string, string>();
private void Awake()
{
for (int i = 0; i < imgContent.childCount; i++)
{
Image img = imgContent.GetChild(i).GetComponent<Image>();
Button imgBtn = img.GetComponent<Button>();
imgBtn.onClick.AddListener(()=>
{
SelectImagePath(img);
});
}
serializeBtn.onClick.AddListener(SerializeImg);
deserializeBtn.onClick.AddListener(DerializeImg);
}
//通过资源浏览器选择一张本地图片
private void SelectImagePath(Image img)
{
ExtensionFilter[] extensions = new ExtensionFilter[] {new ExtensionFilter("Image Files", "jpg", "png") };
string imgPath = FileBrowser.OpenSingleFile("请选择一张图片", "", extensions);
if (File.Exists(imgPath))
{
string md5 = "";
LoadImage(imgPath, ref md5);
img.sprite = spriteDataDics[md5].Sprite;
if (imgObjMaps.ContainsKey(img.name))
{
imgObjMaps[img.name] = md5;
}
else
{
imgObjMaps.Add(img.name, md5);
}
}
}
//解析图片
private void LoadImage(string path, ref string md5)
{
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
var crypto = MD5.Create();
var md5Hash = crypto.ComputeHash(fs);//计算md5
//D4-1D-8C-D9-8F-00-B2-04-E9-80-09-98-EC-F8-42-7E,这是通过BitConverter转的md5字符串,所以需要将其去掉“-”再转为小写形式
md5 = System.BitConverter.ToString(md5Hash).Replace("-", "").ToLower();
if (!spriteDataDics.ContainsKey(md5))
{
fs.Seek(0, SeekOrigin.Begin);//如果计算了md5的值,此时文件流的指针指向最后一位,如果还需要读取流,则需将指针返回文件头
byte[] imgBytes = new byte[fs.Length];
fs.Read(imgBytes, 0, imgBytes.Length);
string imgStr = ByteToStr(imgBytes);
Sprite sprite = ByteToSprite(imgBytes);
AddTospriteDataDics(md5, imgStr,sprite);
}
fs.Close();
}
/// <summary>
/// 添加精灵图数据到缓存字典
/// </summary>
/// <param name="md5"></param>
/// <param name="imgBytes"></param>
private void AddTospriteDataDics(string md5,string imgStr,Sprite sprite)
{
SpriteData spriteData = new SpriteData(imgStr, sprite);
spriteDataDics.Add(md5, spriteData);
}
//字节数组转为一种Base64编码形式的字符串
private string ByteToStr(byte[] bytes)
{
return System.Convert.ToBase64String(bytes);
}
//将Base64编码字符串转为字节数组
private byte[] StrToByte(string str)
{
return System.Convert.FromBase64String(str);
}
//字节数组转精灵图
private Sprite ByteToSprite(byte[] bytes)
{
Texture2D tex = new Texture2D(10,10);
tex.LoadImage(bytes);
Sprite sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
return sprite;
}
//字符串转精灵图
private Sprite StrToSprite(string str)
{
byte[] imgBytes = StrToByte(str);
return ByteToSprite(imgBytes);
}
//序列化图片
private void SerializeImg()
{
string savePath = FileBrowser.SaveFile("请选择保存路径", "", "imgXml", "xml");
if (!string.IsNullOrEmpty(savePath))
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.CreateXmlDeclaration("1.0", "utf-8", "");
xmlDoc.AppendChild(SerializeImgXmlNode(xmlDoc));
xmlDoc.Save(savePath);
}
}
private XmlNode SerializeImgXmlNode(XmlDocument doc)
{
XmlNode imgNode = doc.CreateElement("Image");
//保存图片组件与图片的映射关系
foreach (string objName in imgObjMaps.Keys)
{
XmlElement objNode = doc.CreateElement("Obj");
objNode.SetAttribute("img", objName);
objNode.InnerText = imgObjMaps[objName];
imgNode.AppendChild(objNode);
}
//保存图片的md5值和字符串
foreach (string item in spriteDataDics.Keys)
{
XmlElement itemElement = doc.CreateElement("ImageData");
itemElement.SetAttribute("md5", item);
itemElement.InnerText = spriteDataDics[item].ImgStr;
imgNode.AppendChild(itemElement);
}
return imgNode;
}
//反序列化图片
private void DerializeImg()
{
string openPath = FileBrowser.OpenSingleFile("请选择一个xml文件", "", "xml");
if (File.Exists(openPath))
{
XmlDocument doc = new XmlDocument();
doc.Load(openPath);
DerializeImgXmlNode(doc.FirstChild);
}
}
private void DerializeImgXmlNode(XmlNode node)
{
foreach (XmlNode nodeItem in node.ChildNodes)
{
switch (nodeItem.Name)
{
case "Obj":
{
string objName = nodeItem.Attributes["img"].InnerText;
string md5 = nodeItem.InnerText;
imgObjMaps.Add(objName, md5);
}
break;
case "ImageData":
{
string md5 = nodeItem.Attributes["md5"].Value;
string imgStrData = nodeItem.InnerText;
Sprite sprite = StrToSprite(imgStrData);
AddTospriteDataDics(md5, imgStrData, sprite);
}
break;
}
}
//反序列完成之后显示上次选择的图片
foreach (string objName in imgObjMaps.Keys)
{
if (spriteDataDics.ContainsKey(imgObjMaps[objName]))
{
GameObject.Find(objName).GetComponent<Image>().sprite = spriteDataDics[imgObjMaps[objName]].Sprite;
}
}
}
}
3、demo演示
序列化图片:
我们打开序列化图片保存的xml文件可以看到:成功的将图片组件和图片的md5的映射关系保存下来了;图片数据md5值和对应的字符串也保存下来了。
反序列化图片:
通过反序列化图片打开之前保存的项目xml文件,成功的还原了我们上次选择过的图片。
👉五、未来想要优化的问题
- 计算md5值的时候,我是直接计算的整个图片文件的字节数组得出的md5,目前来说读取0到10兆内的图片没发现有卡顿或者说读取慢的情况。更大的图片文件暂时没测试过。后面可以看看,如果发现读取慢,可以考虑只计算图片字节数组的前面、或者后面的固定长度的字节数据,或者随机选择指定长度的字节数据进行计算md5值。
- 通过Xml方法的序列化与反序列化,确实可读性较强,可以在无其他环境时使用;但随着使用图片资源增多,会导致体积庞大。所以未来可以考虑用其他方法进行序列化与反序列保存(如二进制、Json等),比较后再择优选择方案。
- 当然也可以尝试将数据保存到网络服务器中,不过服务器这块暂时不是很熟悉,但这是未来想学习并尝试的方案。
未完待续…