From 4d481f0379ef57235655a58a7b14bcf16d502cbf Mon Sep 17 00:00:00 2001 From: ALEXTANG <574809918@qq.com> Date: Fri, 10 Jun 2022 17:40:02 +0800 Subject: [PATCH] Hot Update DownLoader Hot Update DownLoader --- .../Runtime/DownloadHandlerFileRange.cs | 179 +++++++++++ .../Runtime/DownloadHandlerFileRange.cs.meta | 11 + .../Runtime/HotUpdate/Runtime/DownloadImpl.cs | 299 ++++++++++++++++++ .../HotUpdate/Runtime/DownloadImpl.cs.meta | 11 + .../Runtime/HotUpdate/Runtime/LoadData.cs | 26 ++ .../HotUpdate/Runtime/LoadData.cs.meta | 11 + .../Runtime/HotUpdate/Runtime/LoadMgr.cs | 98 ++++++ .../Runtime/HotUpdate/Runtime/LoadMgr.cs.meta | 11 + .../HotUpdate/Runtime/LoaderUtilities.cs | 67 ++++ .../HotUpdate/Runtime/LoaderUtilities.cs.meta | 11 + 10 files changed, 724 insertions(+) create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs new file mode 100644 index 00000000..1107bba7 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs @@ -0,0 +1,179 @@ +using System.IO; +using UnityEngine.Networking; + +namespace TEngine +{ + public class DownloadHandlerFileRange : DownloadHandlerScript, IDownload + { + private readonly string _url; + private string _path; + private string _md5; + private FileStream _fileStream; + private UnityWebRequest _unityWebRequest; + + private long _totalFileSize = 0; + private long _curFileSize = 0; + public bool HasError { get; private set; } + protected BackgroundDownloadStatus _status = BackgroundDownloadStatus.NotBegin; + + private DownloadImpl _imp; + + public void SetImp(DownloadImpl imp) + { + _imp = imp; + } + + public DownloadHandlerFileRange(string url, string path, long totalLength, string md5) : base(new byte[1024 * 1024]) + { + TLogger.LogInfo($"DownloadHandlerFileRange url:{url},path:{path},totalLength:{totalLength}"); + + _url = url; + _path = path; + _md5 = md5; + + var dirPath = Path.GetDirectoryName(_path); + if (dirPath != null) + { + TLogger.LogInfo($"DownloadHandlerFileRange dirPath:{dirPath}"); + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + } + + _totalFileSize = totalLength; + _status = BackgroundDownloadStatus.NotBegin; + } + + /// + /// 兼容断点续传 + /// + public void StartDownload() + { + if (_status != BackgroundDownloadStatus.NotBegin) + { + return; + } + + var index = _path.IndexOf("_md5_"); + var fileMd5 = string.Empty; + if (File.Exists(_path)) + { + fileMd5 = LoaderUtilities.GetMd5Hash(_path); + } + else + { + if (index >= 0) + { + fileMd5 = LoaderUtilities.GetMd5Hash(_path.Substring(0, index)); + } + } + + if (fileMd5 == _md5) + { + _status = BackgroundDownloadStatus.Done; + return; + } + + try + { + _fileStream = new FileStream(_path, FileMode.OpenOrCreate, FileAccess.Write); + var localFileSize = _fileStream.Length; + _fileStream.Seek(localFileSize, SeekOrigin.Begin); + _curFileSize = localFileSize; + _unityWebRequest = UnityWebRequest.Get(_url); + _unityWebRequest.SetRequestHeader("Range", "bytes=" + localFileSize + "-" + _totalFileSize); + _unityWebRequest.downloadHandler = this; + _unityWebRequest.SendWebRequest(); + + _status = BackgroundDownloadStatus.Downloading; + } + catch (System.Exception e) + { + TLogger.LogError($"DownloadHandlerFileRange.StartDownload,Exception,{e.StackTrace}"); + _status = BackgroundDownloadStatus.Failed; + throw; + } + } + + public new void Dispose() + { + if (_status == BackgroundDownloadStatus.Downloading) + { + _status = BackgroundDownloadStatus.Failed; + } + + base.Dispose(); + + if (_fileStream != null) + { + _fileStream.Close(); + _fileStream.Dispose(); + _fileStream = null; + } + + if (_unityWebRequest != null) + { + _unityWebRequest.Abort(); + _unityWebRequest.Dispose(); + _unityWebRequest = null; + } + } + #region + public float Progress => _totalFileSize == 0 ? 0 : ((float)_curFileSize) / _totalFileSize; + + public long TotalSize => _totalFileSize; + + public long CurrentSize => _curFileSize; + + protected override bool ReceiveData(byte[] data, int dataLength) + { + if (data == null || dataLength <= 0) + { + return false; + } + _fileStream.Write(data, 0, dataLength); + _fileStream.Flush(); + _curFileSize += dataLength; + LoadUpdateLogic.Instance.Down_Progress_Action?.Invoke(_curFileSize); + + return true; + } + #endregion + #region IEnumerator + public object Current + { + get + { + return null; + } + } + + public BackgroundDownloadStatus Status => _status; + + public bool MoveNext() + { + if (_status == BackgroundDownloadStatus.Done || + _status == BackgroundDownloadStatus.Failed || + _status == BackgroundDownloadStatus.NetworkError) + { + return false; + } + + if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError) + { + _status = BackgroundDownloadStatus.NetworkError; + } + else if (_unityWebRequest.isDone) + { + _status = BackgroundDownloadStatus.Done; + } + + return _status == BackgroundDownloadStatus.Downloading; + } + + public void Reset() + { } + #endregion + } +} diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta new file mode 100644 index 00000000..f71035a2 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13ec5fbe4e3143e479b624eac7b1431e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs new file mode 100644 index 00000000..60e01462 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace TEngine +{ + /// + /// 下载状态 + /// + public enum BackgroundDownloadStatus + { + NotBegin = 0, //未开始 + Downloading = 1, //下载中 + NetworkError = 2, //网络变化 + Done = 3, //下载完成 + Failed = 4, //下载失败 + } + + /// + /// 资源加载过程中的状态 + /// + public enum DownLoadResult + { + StartDownLoad = 0, //开始下载 + HeadRequestFail = 1, //请求头失败 + DownLoadRequestFail = 2, //现在请求失败 + AreadyDownLoaded = 3, //已经下载过而且下载好了 + DownLoading = 4, //下载中 + NetChanged = 5, + DownLoaded = 6, //下载完成 + DownLoadingError = 7,//接收数据的那个过程中出错 + HeadRequestError = 8,//获取下载包大小报错 + ReceiveNullData = 9,//接受到空数据 + DownError = 10,//数据没有接受完但是isDone为true + ReceiveError = 11,//接收数据失败 + Md5Wrong = 12,//md5错误 + AllDownLoaded = 13//全部下载完成 + } + + public class DownloadImpl + { + private List _files; + private string _path; + private Action> _callback = null; + private long _totalFileSize = 0; + private long _downLoadedSize = 0; + private long _currentLoadSize = 0; + + private float _last_record_time = 0f; + private float _last_record_process = 0f; + private long _speed = 0; + + IDownload _downloader = null; + + public DownloadImpl(List files, string path, Action> callback) + { + _files = files; + _path = path; + _callback = callback; + _downLoadedSize = 0; + _totalFileSize = 0; + foreach (var item in files) + { + _totalFileSize += item.Size; + } + + var dirPath = Path.GetDirectoryName(_path); + if (dirPath != null) + { + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + } + } + + public IEnumerator DownLoad() + { + _downLoadedSize = 0; + bool result = false; + foreach (var item in _files) + { + string remoteUrl = item.RemoteUrl + "?" + GameConfig.Instance.GameBundleVersion; + Callback(DownLoadResult.StartDownLoad); + _downloader = GetDownloader(remoteUrl, _path + item.Url, item.Size, item.Md5, this); + _downloader.StartDownload(); + yield return _downloader; + + if (_downloader != null) + { + _downloader.Dispose(); + result = _DealWithDownLoadOk(DownLoadResult.DownLoaded, _downloader.Status, item); + if (result == false) + { + yield break; + } + + if (_downloader.Status == BackgroundDownloadStatus.Done) + { + _downLoadedSize += item.Size; + } + } + yield return null; + } + + if (result) + { + _DownLoaded(_files); + } + } + + void _DownLoaded(List list) + { + Callback(DownLoadResult.AllDownLoaded, list); + } + + bool _DealWithDownLoadOk(DownLoadResult downloadType, BackgroundDownloadStatus status, LoadResource data) + { + string fileLocalPath = _path + data.Url; + + if (status == BackgroundDownloadStatus.NetworkError) + { + Callback(DownLoadResult.NetChanged); + return false; + } + + if (status == BackgroundDownloadStatus.Failed) + { + LoaderUtilities.DeleteFile(fileLocalPath); + TLogger.LogError("DownloaderImpl._DownLoaded, Load failed"); + Callback(DownLoadResult.ReceiveError); + return false; + } + + int index = fileLocalPath.IndexOf("_md5_"); + var tempMd5 = LoaderUtilities.GetMd5Hash(fileLocalPath); + if (index >= 0) + { + var fileInfo = new FileInfo(fileLocalPath); + string newFilename = fileLocalPath.Substring(0, index); + if (tempMd5 == data.Md5) + { + if (File.Exists(newFilename)) + { + File.Delete(newFilename); + } + + fileInfo.MoveTo(newFilename); + Callback(downloadType); + } + else + { + if (File.Exists(newFilename)) + { + return true; + } + else + { + TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5:{tempMd5},Target md5:{data.Md5} not match,path:{data.Url}"); + LoaderUtilities.DeleteFile(fileLocalPath); + Callback(DownLoadResult.Md5Wrong); + return false; + } + } + } + else + { + if (tempMd5 == data.Md5) + { + Callback(downloadType); + } + else + { + TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5:{tempMd5},Target md5:{data.Md5} not match,path:{data.Url}"); + LoaderUtilities.DeleteFile(fileLocalPath); + Callback(DownLoadResult.Md5Wrong); + return false; + } + } + return true; + } + + void Callback(DownLoadResult result, List files = null) + { + _callback?.Invoke((int)result, files); + } + + /// + /// 文件总大小 + /// + public long FileSize + { + get + { + return _totalFileSize; + } + } + + public long DownLoadSize + { + get => _downLoadedSize; + set => _downLoadedSize = value; + } + + public long CurrentLoadSize() + { + return _downLoadedSize + _downloader.CurrentSize; + } + + /// + /// 返回下载速度 + /// + public long Speed + { + get + { + if (_downloader == null) + return 0; + + if (Time.time - _last_record_time < 0.5) + { + return _speed; + } + + float progress = _downloader.Progress; + if (progress == _last_record_process) + { + return _speed; + } + + _speed = (long)((progress - _last_record_process) * _downloader.TotalSize / (Time.time - _last_record_time)); + _last_record_process = progress; + _last_record_time = Time.time; + return _speed; + } + } + + public static IDownload GetDownloader(string url, string path, long totalLength, string md5, DownloadImpl imp) + { + DownloadHandlerFileRange loader = new DownloadHandlerFileRange(url, path, totalLength, md5); + loader.SetImp(imp); + return loader; + } + + public bool IsLoading() + { + if (_downloader == null) + return false; + return _downloader.Status == BackgroundDownloadStatus.Downloading; + } + + public BackgroundDownloadStatus Statue() + { + if (_downloader == null) + return BackgroundDownloadStatus.NotBegin; + return _downloader.Status; + } + + public bool IsNetWorkChanged() + { + if (_downloader == null) + return false; + return _downloader.Status == BackgroundDownloadStatus.NetworkError; + } + + public void StopDownLoad() + { + if (_downloader != null) + { + _downloader.Dispose(); + } + } + + public void Release() + { + _files = null; + _path = ""; + _callback = null; + _totalFileSize = 0; + StopDownLoad(); + } + } + + public interface IDownload : IEnumerator + { + float Progress { get; } + long TotalSize { get; } + long CurrentSize { get; } + BackgroundDownloadStatus Status { get; } + + void Dispose(); + void StartDownload(); + } +} diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta new file mode 100644 index 00000000..678f2020 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a6103e1339c9a164ba4dc41f33be1cf3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs new file mode 100644 index 00000000..cff8cda3 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TEngine +{ + public enum GameStatus + { + First = 0, + AssetLoad = 1 + } + + public struct LoadResource + { + public string Url;//资源名称 + public string Md5;//资源的md5码 + public long Size; //资源大小(字节为单位) + public string RemoteUrl;//服务器地址 + } + + public class LoadData + { + } +} diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta new file mode 100644 index 00000000..d91e11b1 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c685f02226943e946853424f953dcfee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs new file mode 100644 index 00000000..4103dab7 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; + +namespace TEngine +{ + public class LoadUpdateLogic + { + private static LoadUpdateLogic _instance; + + public Action Download_Complete_Action = null; + public Action Down_Progress_Action = null; + public Action _Unpacked_Complete_Action = null; + public Action _Unpacked_Progress_Action = null; + + public static LoadUpdateLogic Instance + { + get + { + if (_instance == null) + _instance = new LoadUpdateLogic(); + return _instance; + } + } + } + + public class LoadMgr : TSingleton + { + /// + /// 资源版本号 + /// + public string LatestResId { get; set; } + + private Action _startGameEvent; + private int _curTryCount; + private const int MaxTryCount = 3; + private bool _connectBack; + private bool _needUpdate = false; + + public LoadMgr() + { + _curTryCount = 0; + _connectBack = false; + _startGameEvent = null; + } + + public void StartLoadInit(Action onUpdateComplete) + { +#if RELEASE_BUILD || _DEVELOPMENT_BUILD_ + StartLoad(() => { FinishCallBack(onUpdateComplete); }); +#else + onUpdateComplete(); +#endif + } + + /// + /// 开启热更新逻辑 + /// + /// + public void StartLoad(Action action) + { + _startGameEvent = action; + _connectBack = false; + _curTryCount = 0; + RequestVersion(); + } + + private void FinishCallBack(Action callBack) + { + GameConfig.Instance.WriteVersion(LatestResId); + if (_needUpdate) + { + callBack(); + } + else + { + callBack(); + } + } + + /// + /// 请求热更数据 + /// + private void RequestVersion() + { + if (_connectBack) + { + return; + } + + _curTryCount++; + + if (_curTryCount > MaxTryCount) + { + + } + } + } +} diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta new file mode 100644 index 00000000..85cb03f5 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56d713df43471c642a913a349900d028 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs new file mode 100644 index 00000000..3d2739f2 --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace TEngine +{ + public class LoaderUtilities + { + + /// + /// 删除文件 + /// + /// 文件路径 + public static void DeleteFile(string filePath) + { + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + catch (Exception e) + { + TLogger.LogError(e.ToString()); + } + } + + /// + /// 获取文件的md5码 + /// + /// + /// + public static string GetMd5Hash(string fileName) + { + if (!File.Exists(fileName)) + { + TLogger.LogWarning($"not exit file,path:{fileName}"); + return string.Empty; + } + try + { + using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] retVal = md5.ComputeHash(file); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + } + + } + catch (Exception ex) + { + TLogger.LogError("GetMD5Hash() fail,error:" + ex.Message); + return string.Empty; + } + } + } +} diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta new file mode 100644 index 00000000..07fa06ef --- /dev/null +++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 186c876c2ef7f2f4db981fd403e22ce7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: