一、准备工作
导入ICSharpCode.SharpZipLib.dll。
二、zip压缩代码
using System.IO;
using System.Collections;
using UnityEngine;
using ICSharpCode.SharpZipLib.Zip;
namespace Ardez
{
public static class ZipUtility
{
#region ZipCallback
public abstract class ZipCallback
{
/// <summary>
/// 压缩单个文件或文件夹前执行的回调
/// </summary>
/// <param name="_entry"></param>
/// <returns>如果返回true,则压缩文件或文件夹,反之则不压缩文件或文件夹</returns>
public virtual bool OnPreZip(ZipEntry _entry)
{
return true;
}
/// <summary>
/// 压缩单个文件或文件夹后执行的回调
/// </summary>
/// <param name="_entry"></param>
public virtual void OnPostZip(ZipEntry _entry) { }
/// <summary>
/// 压缩执行完毕后的回调
/// </summary>
/// <param name="_result">true表示压缩成功,false表示压缩失败</param>
public virtual void OnFinished(bool _result) { }
}
#endregion
#region UnzipCallback
public abstract class UnzipCallback
{
/// <summary>
/// 解压单个文件或文件夹前执行的回调
/// </summary>
/// <param name="_entry"></param>
/// <returns>如果返回true,则压缩文件或文件夹,反之则不压缩文件或文件夹</returns>
public virtual bool OnPreUnzip(ZipEntry _entry)
{
return true;
}
/// <summary>
/// 解压单个文件或文件夹后执行的回调
/// </summary>
/// <param name="_entry"></param>
public virtual void OnPostUnzip(ZipEntry _entry) { }
/// <summary>
/// 解压执行完毕后的回调
/// </summary>
/// <param name="_result">true表示解压成功,false表示解压失败</param>
public virtual void OnFinished(bool _result) { }
}
#endregion
/// <summary>
/// 压缩文件和文件夹
/// </summary>
/// <param name="_fileOrDirectoryArray">文件夹路径和文件名</param>
/// <param name="_outputPathName">压缩后的输出路径文件名</param>
/// <param name="_password">压缩密码</param>
/// <param name="_zipCallback">ZipCallback对象,负责回调</param>
/// <returns></returns>
public static bool Zip(string[] _fileOrDirectoryArray, string _outputPathName, string _password = null, ZipCallback _zipCallback = null)
{
if ((null == _fileOrDirectoryArray) || string.IsNullOrEmpty(_outputPathName))
{
if (null != _zipCallback)
_zipCallback.OnFinished(false);
return false;
}
ZipOutputStream zipOutputStream = new ZipOutputStream(File.Create(_outputPathName));
zipOutputStream.SetLevel(6); // 压缩质量和压缩速度的平衡点
if (!string.IsNullOrEmpty(_password))
zipOutputStream.Password = _password;
for (int index = 0; index < _fileOrDirectoryArray.Length; ++index)
{
bool result = false;
string fileOrDirectory = _fileOrDirectoryArray[index];
if (Directory.Exists(fileOrDirectory))
result = ZipDirectory(fileOrDirectory, string.Empty, zipOutputStream, _zipCallback);
else if (File.Exists(fileOrDirectory))
result = ZipFile(fileOrDirectory, string.Empty, zipOutputStream, _zipCallback);
if (!result)
{
if (null != _zipCallback)
_zipCallback.OnFinished(false);
return false;
}
}
zipOutputStream.Finish();
zipOutputStream.Close();
if (null != _zipCallback)
_zipCallback.OnFinished(true);
return true;
}
/// <summary>
/// 解压Zip包
/// </summary>
/// <param name="_filePathName">Zip包的文件路径名</param>
/// <param name="_outputPath">解压输出路径</param>
/// <param name="_password">解压密码</param>
/// <param name="_unzipCallback">UnzipCallback对象,负责回调</param>
/// <returns></returns>
public static bool UnzipFile(string _filePathName, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null)
{
if (string.IsNullOrEmpty(_filePathName) || string.IsNullOrEmpty(_outputPath))
{
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
return false;
}
try
{
return UnzipFile(File.OpenRead(_filePathName), _outputPath, _password, _unzipCallback);
}
catch (System.Exception _e)
{
Debug.LogError("[ZipUtility.UnzipFile]: " + _e.ToString());
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
return false;
}
}
/// <summary>
/// 解压Zip包
/// </summary>
/// <param name="_fileBytes">Zip包字节数组</param>
/// <param name="_outputPath">解压输出路径</param>
/// <param name="_password">解压密码</param>
/// <param name="_unzipCallback">UnzipCallback对象,负责回调</param>
/// <returns></returns>
public static bool UnzipFile(byte[] _fileBytes, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null)
{
if ((null == _fileBytes) || string.IsNullOrEmpty(_outputPath))
{
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
return false;
}
bool result = UnzipFile(new MemoryStream(_fileBytes), _outputPath, _password, _unzipCallback);
if (!result)
{
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
}
return result;
}
/// <summary>
/// 解压Zip包
/// </summary>
/// <param name="_inputStream">Zip包输入流</param>
/// <param name="_outputPath">解压输出路径</param>
/// <param name="_password">解压密码</param>
/// <param name="_unzipCallback">UnzipCallback对象,负责回调</param>
/// <returns></returns>
public static bool UnzipFile(Stream _inputStream, string _outputPath, string _password = null, UnzipCallback _unzipCallback = null)
{
if ((null == _inputStream) || string.IsNullOrEmpty(_outputPath))
{
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
return false;
}
// 创建文件目录
if (!Directory.Exists(_outputPath))
Directory.CreateDirectory(_outputPath);
// 解压Zip包
ZipEntry entry = null;
using (ZipInputStream zipInputStream = new ZipInputStream(_inputStream))
{
if (!string.IsNullOrEmpty(_password))
zipInputStream.Password = _password;
while (null != (entry = zipInputStream.GetNextEntry()))
{
if (string.IsNullOrEmpty(entry.Name))
continue;
if ((null != _unzipCallback) && !_unzipCallback.OnPreUnzip(entry))
continue; // 过滤
string filePathName = Path.Combine(_outputPath, entry.Name);
// 创建文件目录
if (entry.IsDirectory)
{
Directory.CreateDirectory(filePathName);
continue;
}
// 写入文件
try
{
using (FileStream fileStream = File.Create(filePathName))
{
byte[] bytes = new byte[1024];
while (true)
{
int count = zipInputStream.Read(bytes, 0, bytes.Length);
if (count > 0)
fileStream.Write(bytes, 0, count);
else
{
if (null != _unzipCallback)
_unzipCallback.OnPostUnzip(entry);
break;
}
}
}
}
catch (System.Exception _e)
{
Debug.LogError("[ZipUtility.UnzipFile]: " + _e.ToString());
if (null != _unzipCallback)
_unzipCallback.OnFinished(false);
return false;
}
}
}
if (null != _unzipCallback)
_unzipCallback.OnFinished(true);
return true;
}
/// <summary>
/// 压缩文件
/// </summary>
/// <param name="_filePathName">文件路径名</param>
/// <param name="_parentRelPath">要压缩的文件的父相对文件夹</param>
/// <param name="_zipOutputStream">压缩输出流</param>
/// <param name="_zipCallback">ZipCallback对象,负责回调</param>
/// <returns></returns>
private static bool ZipFile(string _filePathName, string _parentRelPath, ZipOutputStream _zipOutputStream, ZipCallback _zipCallback = null)
{
//Crc32 crc32 = new Crc32();
ZipEntry entry = null;
FileStream fileStream = null;
try
{
string entryName = _parentRelPath + '/' + Path.GetFileName(_filePathName);
entry = new ZipEntry(entryName);
entry.DateTime = System.DateTime.Now;
if ((null != _zipCallback) && !_zipCallback.OnPreZip(entry))
return true; // 过滤
fileStream = File.OpenRead(_filePathName);
byte[] buffer = new byte[fileStream.Length];
fileStream.Read(buffer, 0, buffer.Length);
fileStream.Close();
entry.Size = buffer.Length;
//crc32.Reset();
//crc32.Update(buffer);
//entry.Crc = crc32.Value;
_zipOutputStream.PutNextEntry(entry);
_zipOutputStream.Write(buffer, 0, buffer.Length);
}
catch (System.Exception _e)
{
Debug.LogError("[ZipUtility.ZipFile]: " + _e.ToString());
return false;
}
finally
{
if (null != fileStream)
{
fileStream.Close();
fileStream.Dispose();
}
}
if (null != _zipCallback)
_zipCallback.OnPostZip(entry);
return true;
}
/// <summary>
/// 压缩文件夹
/// </summary>
/// <param name="_path">要压缩的文件夹</param>
/// <param name="_parentRelPath">要压缩的文件夹的父相对文件夹</param>
/// <param name="_zipOutputStream">压缩输出流</param>
/// <param name="_zipCallback">ZipCallback对象,负责回调</param>
/// <returns></returns>
private static bool ZipDirectory(string _path, string _parentRelPath, ZipOutputStream _zipOutputStream, ZipCallback _zipCallback = null)
{
ZipEntry entry = null;
try
{
string entryName = Path.Combine(_parentRelPath, Path.GetFileName(_path) + '/');
entry = new ZipEntry(entryName);
entry.DateTime = System.DateTime.Now;
entry.Size = 0;
if ((null != _zipCallback) && !_zipCallback.OnPreZip(entry))
return true; // 过滤
_zipOutputStream.PutNextEntry(entry);
_zipOutputStream.Flush();
string[] files = Directory.GetFiles(_path);
for (int index = 0; index < files.Length; ++index)
ZipFile(files[index], Path.Combine(_parentRelPath, Path.GetFileName(_path)), _zipOutputStream, _zipCallback);
}
catch (System.Exception _e)
{
Debug.LogError("[ZipUtility.ZipDirectory]: " + _e.ToString());
return false;
}
string[] directories = Directory.GetDirectories(_path);
for (int index = 0; index < directories.Length; ++index)
{
if (!ZipDirectory(directories[index], Path.Combine(_parentRelPath, Path.GetFileName(_path)), _zipOutputStream, _zipCallback))
return false;
}
if (null != _zipCallback)
_zipCallback.OnPostZip(entry);
return true;
}
}
}
三、压缩运用
些了一个操作类ZipManager.cs来处理压缩与加压逻辑
public static class ZipManager
{
/// <summary>
/// 加密密码
/// </summary>
public static readonly string passedWord = "xyh";
/// <summary>
/// 压缩
/// </summary>
/// <param name="fileAsset">要解压缩的文件路径</param>
/// <param name="outPutPath">压缩输出全路径(包括后缀名)</param>
public static void ZipFile(string[] fileAsset, string outPutPath)
{
ZipUtility.Zip(fileAsset, outPutPath, passedWord);
}
/// <summary>
/// 解压
/// </summary>
/// <param name="filepath">要解压的文件包</param>
/// <param name="outPutPath">输出路径</param>
public static void UnzipFile(string filepath, string outPutPath)
{
ZipUtility.UnzipFile(filepath, outPutPath, passedWord);
}
}
操作测试代码
public class ZipTest : MonoBehaviour
{
string[] _fileOrDirectoryArray = new string[] { @"D:\ZipTest\测试文件" };
/// <summary>
/// 输出路径
/// </summary>
string outputPath = @"D:\ZipTest\Output";
/// <summary>
/// 压缩路径
/// </summary>
string zipPath = @"D:\ZipTest\我的文件.xyh";
public void OnZipClick()
{
ZipManager.ZipFile(_fileOrDirectoryArray, zipPath);
}
public void OnUnZipClick()
{
ZipManager.UnzipFile(zipPath, outputPath);
}
}
这样压缩与解压基本操作就是实现了。
四、线程压缩
但是如上这样压缩与解压时整个程序会卡主,直到压缩与解压完成。
最开始我想到的是用协程,但发现仍然会卡主。
其实协程实在主线程闲置时运行的,线程才是独立运行的。所以这里就需要使用到线程来进行压缩与解压的操作。
改造 ZipManager 类:
using System.Threading;
public static class ZipManager
{
/// <summary>
/// 加密密码
/// </summary>
public static readonly string passedWord = "xyh";
/// <summary>
/// 压缩
/// </summary>
/// <param name="fileAsset">要解压缩的文件路径</param>
/// <param name="outPutPath">压缩输出全路径(包括后缀名)</param>
public static void ZipFile(string[] fileAsset, string outPutPath)
{
ThreadPool.QueueUserWorkItem((p) =>
{
ZipUtility.Zip(fileAsset, outPutPath, passedWord);
});
}
/// <summary>
/// 解压
/// </summary>
/// <param name="filepath">要解压的文件包</param>
/// <param name="outPutPath">输出路径</param>
public static void UnzipFile(string filepath, string outPutPath)
{
ThreadPool.QueueUserWorkItem((p) =>
{
ZipUtility.UnzipFile(filepath, outPutPath, passedWord);
});
}
}
如上使用了一个线程池,整个进度就丝滑起来了。
五、获取压缩进度
但是新问题又来了,虽然在压缩与解压时不会影响到主线程,可我不知道什么时候做完了操作。
我必须要获取到压缩与解压时的进度。
在 ZipUtility 类中有两个回调委托,ZipCallback 和 UnzipCallback 可以获得压缩与解压的开始、结束和解压好的文件。
为了能够时候计算到进度值,我对这两个回调类进行的扩展:
using ICSharpCode.SharpZipLib.Zip;
using System;
using UnityEngine;
using static ZipUtility;
public class ZipCallbackEvent : ZipCallback
{
public Action<long> OnZip;
public Action<bool> OnZipFinish;
public long zipSize;
public override bool OnPreZip(ZipEntry _entry)
{
base.OnPostZip(_entry);
return true;
}
public override void OnPostZip(ZipEntry _entry)
{
base.OnPostZip(_entry);
OnZip?.Invoke(_entry.Size);
}
public override void OnFinished(bool _result)
{
base.OnFinished(_result);
OnZipFinish?.Invoke(_result);
}
}
using ICSharpCode.SharpZipLib.Zip;
using System;
using UnityEngine;
using static ZipUtility;
public class UnZipCallbackEvent : UnzipCallback
{
public Action<long> OnUnZip;
public Action<bool> OnUnZipFinish;
public long unZipSize;
public override void OnPostUnzip(ZipEntry _entry)
{
base.OnPostUnzip(_entry);
OnUnZip?.Invoke(_entry.Size);
}
public override void OnFinished(bool _result)
{
base.OnFinished(_result);
OnUnZipFinish?.Invoke(_result);
}
}
这样就能实时获取到解压和压缩好的文件大小。
这时还需要获取整个文件的大小,如下写了一个工具类ResUtility:
using UnityEngine;
using System;
using System.IO;
public static class ResUtility
{
/// <summary>
/// 删除文件夹内资源
/// </summary>
/// <param name="filePath"></param>
public static void ClearFile(string filePath)
{
if (Directory.Exists(filePath))
{
try
{
var dir = new DirectoryInfo(filePath);
dir.Attributes = dir.Attributes & ~FileAttributes.ReadOnly;
dir.Delete(true);
}
catch (Exception ex)
{
Debug.LogError(string.Format(" 文件夹存在 删除文件夹时 出现错误 【{0}】", ex.Message));
}
}
}
/// <summary>
/// 计算文件或文件夹大小
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static long CalculateSize(string path)
{
if (Directory.Exists(path))
{
return Calculate(path);
}
else if (File.Exists(path))
{
return File.ReadAllBytes(path).Length;
}
else
{
return 0;
}
}
private static long Calculate(string path)
{
long size = 0;
//如果是文件,直接计算大小
string[] files = Directory.GetFiles(path);
for (int index = 0; index < files.Length; ++index)
{
size += File.ReadAllBytes(files[index]).Length;
}
//如果是文件夹,再继续遍历
string[] directories = Directory.GetDirectories(path);
for (int index = 0; index < directories.Length; ++index)
{
size += Calculate(directories[index]);
}
return size;
}
}
接下来就是改造 ZipManager 类了
using System;
using System.IO;
using System.Threading;
using UnityEngine;
public static class ZipManager
{
/// <summary>
/// 加密密码
/// </summary>
public static readonly string passedWord = "xyh";
public static ZipCallbackEvent zipCallback = new ZipCallbackEvent();
public static UnZipCallbackEvent unZipCallback = new UnZipCallbackEvent();
/// <summary>
/// 压缩
/// </summary>
/// <param name="fileAsset">要解压缩的文件路径</param>
/// <param name="outPutPath">压缩输出全路径(包括后缀名)</param>
public static void ZipFile(string[] fileAsset, string outPutPath, Action action = null)
{
ThreadPool.QueueUserWorkItem((p) =>
{
long fileSize = ResUtility.CalculateSize(fileAsset[0]);
long totalZipSize = 0;
zipCallback.OnZip = (zipSize) => {
totalZipSize += zipSize;
float progress = totalZipSize * 1.0f / fileSize;
Debug.Log(zipSize + " " + fileSize + " 压缩进度:" + progress);
};
zipCallback.OnZipFinish = (result) =>
{
if (result)
{
Debug.Log("压缩成功!");
action?.Invoke();
}
else
{
Debug.Log("压缩失败!");
}
};
ZipUtility.Zip(fileAsset, outPutPath, passedWord, zipCallback);
});
}
/// <summary>
/// 解压
/// </summary>
/// <param name="filepath">要解压的文件包</param>
/// <param name="outPutPath">输出路径</param>
public static void UnzipFile(string filepath, string outPutPath, Action action = null)
{
ThreadPool.QueueUserWorkItem((p) =>
{
if (Directory.Exists(outPutPath))
{
ResUtility.ClearFile(outPutPath);
}
Directory.CreateDirectory(outPutPath);
long fileSize = ResUtility.CalculateSize(filepath); //获取文件总大小
long totalUnZipSize = 0; //已解压的文件大小
unZipCallback.OnUnZip = (unZipSize) => {
totalUnZipSize += unZipSize;
float progress = totalUnZipSize * 1.0f / fileSize;
Debug.Log(unZipSize + " " + fileSize + " 解压进度:" + progress);
};
unZipCallback.OnUnZipFinish = (result) =>
{
if (result)
{
Debug.Log("解压成功!");
action?.Invoke();
}
else
{
Debug.Log("解压失败!");
}
};
ZipUtility.UnzipFile(filepath, outPutPath, passedWord, unZipCallback);
});
}
}
这样在获得进度的同时,用Action委托来执行压缩解压后的操作。
六、线程中访问Unity组件
在实际开发中遇到个问题,就是Action委托后的的方法中有可能操作Unity的组件,但这个委托又是在线程中的,所以无法访问Unity组件。
这时就需要把委托传递到其他类上执行,如下该着Action的调用:
Loom.QueueOnMainThread((param) =>
{
action?.Invoke();
}, null);
Loom ## 标题参考:https://zhuanlan.zhihu.com/p/107698641
/// <summary>
/// Loom 线程内访问Unity组件
/// </summary>
/// <remarks>
/// 2021.6.03: 创建. 辛羊华 <br/>
/// </remarks>
using UnityEngine;
using System.Collections.Generic;
using System;
using System.Threading;
using System.Linq;
namespace Ardez
{
public class Loom : UnitySingleton<Loom>
{
//是否已经初始化
static bool isInitialized;
private static Loom _ins;
public static Loom ins { get { Initialize(); return _ins; } }
public override void Awake()
{
base.Awake();
_ins = this;
isInitialized = true;
}
//初始化
public static void Initialize()
{
if (!isInitialized)
{
if (!Application.isPlaying)
return;
isInitialized = true;
var obj = new GameObject("Loom");
_ins = obj.AddComponent<Loom>();
DontDestroyOnLoad(obj);
}
}
//单个执行单元(无延迟)
struct NoDelayedQueueItem
{
public Action<object> action;
public object param;
}
//全部执行列表(无延迟)
List<NoDelayedQueueItem> listNoDelayActions = new List<NoDelayedQueueItem>();
//单个执行单元(有延迟)
struct DelayedQueueItem
{
public Action<object> action;
public object param;
public float time;
}
//全部执行列表(有延迟)
List<DelayedQueueItem> listDelayedActions = new List<DelayedQueueItem>();
//加入到主线程执行队列(无延迟)
public static void QueueOnMainThread(Action<object> taction, object param)
{
QueueOnMainThread(taction, param, 0f);
}
//加入到主线程执行队列(有延迟)
public static void QueueOnMainThread(Action<object> action, object param, float time)
{
if (time != 0)
{
lock (ins.listDelayedActions)
{
ins.listDelayedActions.Add(new DelayedQueueItem { time = Time.time + time, action = action, param = param });
}
}
else
{
lock (ins.listNoDelayActions)
{
ins.listNoDelayActions.Add(new NoDelayedQueueItem { action = action, param = param });
}
}
}
//当前执行的无延时函数链
List<NoDelayedQueueItem> currentActions = new List<NoDelayedQueueItem>();
//当前执行的有延时函数链
List<DelayedQueueItem> currentDelayed = new List<DelayedQueueItem>();
void Update()
{
if (listNoDelayActions.Count > 0)
{
lock (listNoDelayActions)
{
currentActions.Clear();
currentActions.AddRange(listNoDelayActions);
listNoDelayActions.Clear();
}
for (int i = 0; i < currentActions.Count; i++)
{
currentActions[i].action(currentActions[i].param);
}
}
if (listDelayedActions.Count > 0)
{
lock (listDelayedActions)
{
currentDelayed.Clear();
currentDelayed.AddRange(listDelayedActions.Where(d => Time.time >= d.time));
for (int i = 0; i < currentDelayed.Count; i++)
{
listDelayedActions.Remove(currentDelayed[i]);
}
}
for (int i = 0; i < currentDelayed.Count; i++)
{
currentDelayed[i].action(currentDelayed[i].param);
}
}
}
void OnDisable()
{
if (_ins == this)
{
_ins = null;
}
}
}
}