在这篇文章之前,可以转到我的这两篇博客:C#热更方案 HybridCLR尝鲜:Windows及Android打包、超详细的Unity3D热更新框架,附示例链接,小白也能看的懂_鹿野素材屋的博客-CSDN博客_热更新框架
这两篇博客看完后,应该就会对热更有个大致的印象了,接下来我们要做的就是将两者合并起来,实现真正的热更。
首先我们要在脚本加载之前加载出所有的脚本文件,MD5效验部分就不再赘叙,具体代码如下:
- using HybridCLR;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using UnityEngine;
- using UnityEngine.Networking;
- using System.Text;
- using System.Security.Cryptography;//包含MD5库
-
- //MD5信息
- [Serializable]
- public class MD5Message
- {
- public string file;//文件位置及名字
- public string md5;//MD5效验结果
- public string fileLength;//文件长度
- }
-
- //MD5全部信息
- [Serializable]
- public class FileMD5
- {
- public string length;//总长度
- public MD5Message[] files;
- }
-
- public static class UrlConfig
- {
- public static string urlPre = "http://192.168.6.123/Chief/MD5";//一些基本的网络配置
-
- }
-
- ///
- /// 初始化加载dll脚本:这里如果需要的话,最好是把表格数据加载也放到最开始
- ///
- public class DllLogin : MonoBehaviour
- {
-
- //所有引用
- public static List<string> AOTMetaAssemblyNames { get; } = new List<string>()
- {
- "mscorlib.dll",
- "System.dll",
- "System.Core.dll",
- };
-
- void Start()
- {
- //先进行热更
- Debug.Log("热更");
- StartCoroutine(VersionUpdate());
- }
-
- #region 热更部分
- ///
- /// 进度
- ///
- public float progress;
- ///
- /// 版本更新
- ///
- IEnumerator VersionUpdate()
- {
- Debug.Log(GetTerracePath());
- //这里做一些本地化处理
- #if UNITY_EDITOR_WIN
- pathName = "Android";
- #elif UNITY_EDITOR_OSX
- pathName = "IOS";
- #elif UNITY_ANDROID
- pathName = "Android";
- #elif UNITY_IOS
- pathName = "IOS";
- #endif
-
- Debug.Log(UrlConfig.urlPre);
-
- var _isGet = true;
- var uri = HttpDownLoadUrl("Bundle.txt");
- Debug.Log(uri);
- var request = UnityWebRequest.Get(uri);
- yield return request.SendWebRequest();
- _isGet = !(request.result == UnityWebRequest.Result.ConnectionError);
- if (_isGet)
- {
- int allfilesLength = 0;
- var _txt = request.downloadHandler.text;
- Debug.Log("拿到txt:");
- //这里要通过MD5效验先拿到所有的代码数据
- List
bims = new List(); - FileMD5 date = JsonUtility.FromJson
(_txt); - var _files = date.files;
- //初始热更 和岛屿升级热更
- var _list = ReviseMD5List(_files);
- Debug.LogError("需要热更长度" + _list.Count);
- DeleteOtherBundles(_files);//删除所有不受版本控制的文件
- string md5, file, path;
- int lenth;
- for (int i = 0; i < _list.Count; i++)
- {
- MD5Message _md5 = _list[i];
- //Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
- file = _md5.file;
- path = PathUrl(file);
- md5 = GetMD5HashFromFile(path);
- //Debug.LogError(file + " " + path + " " + md5);
- if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
- {
- //Debug.LogError("需要添加" + string.IsNullOrEmpty(md5) + " " + md5 + _md5.md5 + " " + md5 != _md5.md5);
- bims.Add(new BundleInfo()
- {
- Url = HttpDownLoadUrl(file),
- Path = path
- });
- lenth = int.Parse(_md5.fileLength);
- allfilesLength += lenth;
- }
- }
- Debug.Log(allfilesLength / (1024 * 1024));
- Debug.LogError("热更资源数量" + bims.Count);
-
- if (bims.Count <= 1) progress = 1;
- //Debug.LogError("......." + GameProp.Inst.isLandUpdate);
- //当有新热更资源需要更新时,自动调用岛屿升级回调??????????
- if (bims.Count > 0)
- {
- //Debug.LogError("开始尝试更新");
- //if (bims.Count != 1) UIMainProp.Inst.isResUpdate = true;
-
- StartCoroutine(DownLoadBundleFiles(bims, (progress) =>
- {
- Debug.Log("自动更新中..."+progress+":"+allfilesLength);
- }, (isfinish) =>
- {
- if (isfinish)
- {
- StartCoroutine(DownLoadAssets(this.StartGame));
- }
- else
- StartCoroutine(VersionUpdate());
- }));
- }
- else
- {
- StartCoroutine(DownLoadAssets(this.StartGame));
- }
-
- }
- }
-
- ///
- /// 删除所有不受版本控制的所有文件
- ///
- void DeleteOtherBundles(/*FileMD5 _md5*/MD5Message[] _list)
- {
- Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
- var _r = "/Android/";
-
- try
- {
- string[] bundleFiles = Directory.GetFiles(GetTerracePath() + _r, "*.*", SearchOption.AllDirectories);
- Debug.LogError("文件名校对 :长度" + bundleFiles.Length + "::::" + GetTerracePath());
- foreach (string idx in bundleFiles)
- {
- try
- {
- var _s = idx.Replace(GetTerracePath() + _r, "");
- _s = _s.Replace(@"\", "/");
- //Debug.Log(idx+":"+ _s + ":" + Directory.Exists(_s));
- if (/*Directory.Exists(_s) &&*/ !FindNameInFileMD5(_list, _s))
- {
- Debug.LogError(idx + "即将被删除");
- File.Delete(idx);
- }
- }
- catch
- {
- Debug.Log("删除文件报错" + idx);
- }
- }
- }
- catch
- {
- Debug.Log("没有android文件夹");
- }
-
- //Debug.Log("~~~~~~~结束删除~~~~~~~");
- }
- static bool FindNameInFileMD5(MD5Message[] _list, string _name)
- {
- foreach (var _m in _list)
- {
- //Debug.LogError("文件名校对" + _m.file + ":::" + _name);
- if (_m.file == _name) return true;
- }
- return false;
- }
-
- public string GetMD5HashFromFile(string fileName)
- {
- if (File.Exists(fileName))
- {
- FileStream file = new FileStream(fileName, FileMode.Open);
- MD5 md5 = new MD5CryptoServiceProvider();
- byte[] retVal = md5.ComputeHash(file);
- file.Close();
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < retVal.Length; i++)
- sb.Append(retVal[i].ToString("x2"));
- return sb.ToString();
- }
- return null;
- }
-
- ///
- /// 校正MD5 list
- ///
- public List
ReviseMD5List(MD5Message[] _list) - {
- var _checkList = new List
(); - var assets = new List<string>
- {
- "prefabs",
- "Assembly-CSharp.dll",
- };
- foreach (var _idx in _list)
- {
- foreach (var _i in assets)
- {
- var _len = _idx.file.Split('/');
- var _end = _len[_len.Length - 1];
- if (_idx.file.Contains("11")) Debug.Log(_idx.file + " ::: " + (_end.Contains(_i) + " : " + !_end.Contains("public_uitoy") + " :: " + _end != "Bundle.txt"));
-
- if (_end.Contains(_i))
- {
- _checkList.Add(_idx);
- break;
- }
- }
- }
-
- Debug.Log("检验列表" + _checkList.Count);
- return _checkList;
- }
-
- ///
- /// 路径比对
- ///
- public IEnumerator DownLoadBundleFiles(List
infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null ) - {
- //Debug.Log("开始路径对比" + infos.Count);
- int num = 0;
- string dir;
- for (int i = 0; i < infos.Count; i++)
- {
- BundleInfo info = infos[i];
- Debug.LogError(info.Url + " :::: " + i);
-
- var uri = info.Url;
- var request = UnityWebRequest.Get(uri);
- yield return request.SendWebRequest();
-
- var _isGet = !(request.result == UnityWebRequest.Result.ConnectionError);
- if (_isGet)
- {
- try
- {
- string filepath = info.Path;
- dir = Path.GetDirectoryName(filepath);
- if (!Directory.Exists(dir))
- Directory.CreateDirectory(dir);
- File.WriteAllBytes(filepath, request.downloadHandler.data);
- num++;
- if (LoopCallBack != null)
- LoopCallBack.Invoke((float)num / infos.Count);
- //Debug.Log(dir + "下载完成");
- }
- catch (Exception e)
- {
- Debug.Log("下载失败"+e);
- }
- }
- else
- {
- Debug.Log("下载失败");
- }
- }
-
- if (CallBack != null)
- CallBack.Invoke(num == infos.Count);
- }
-
-
- ///
- /// 记录信息
- ///
- public struct BundleInfo
- {
- public string Path { get; set; }
- public string Url { get; set; }
- public override bool Equals(object obj)
- {
- return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
- }
- public override int GetHashCode()
- {
- return Url.GetHashCode();
- }
- }
-
-
- ///
- /// 获得下载的URL
- ///
- public string HttpDownLoadUrl(string _str)
- {
- return UrlConfig.urlPre + pathName + "/" + _str;
- }
-
- public string pathName;
-
- //根据不同路径,对下载路径进行封装
- string PathUrl(string _str)
- {
- var _path = GetTerracePath() + "/" + pathName + "/" + _str;
- return _path;
- }
-
- //获得不同平台的路径
- string GetTerracePath()
- {
- string sPath = Application.persistentDataPath;
- return sPath;
- }
-
- #endregion
-
-
- private static Dictionary<string, byte[]> s_assetDatas = new Dictionary<string, byte[]>();
-
- public static byte[] GetAssetData(string dllName)
- {
- return s_assetDatas[dllName];
- }
-
- private string GetWebRequestPath(string asset)
- {
- var path = $"{Application.streamingAssetsPath}/{asset}";
- if (!path.Contains("://"))
- {
- path = "file://" + path;
- }
- return path;
- }
-
-
- IEnumerator DownLoadAssets(Action onDownloadComplete)
- {
- var assets = new List<string>
- {
- "prefabs",
- "Assembly-CSharp.dll",
- }.Concat(AOTMetaAssemblyNames);
-
-
- foreach (var asset in assets)
- {
- string dllPath = GetWebRequestPath(asset);
- //这个地方改为读本地文件夹
- if(asset== "prefabs" || asset== "Assembly-CSharp.dll") dllPath = Path.Combine("file://" + GetTerracePath(), pathName + "/Dll/" + asset);
- //var dllPath = Path.Combine("file://" + GetTerracePath(), pathName + "/Dll/" + asset);
- Debug.Log($"start download asset:{dllPath}");
- UnityWebRequest www = UnityWebRequest.Get(dllPath);
- yield return www.SendWebRequest();
-
- #if UNITY_2020_1_OR_NEWER
- if (www.result != UnityWebRequest.Result.Success)
- {
- Debug.Log(www.error);
- }
- #else
- if (www.isHttpError || www.isNetworkError)
- {
- Debug.Log(www.error);
- }
- #endif
- else
- {
- // Or retrieve results as binary data
- byte[] assetData = www.downloadHandler.data;
- Debug.Log($"dll:{asset} size:{assetData.Length}");
- s_assetDatas[asset] = assetData;
- }
- }
-
- onDownloadComplete();
- }
-
-
- void StartGame()
- {
- LoadMetadataForAOTAssemblies();
-
- #if !UNITY_EDITOR
- var gameAss = System.Reflection.Assembly.Load(GetAssetData("Assembly-CSharp.dll"));
- #else
- var gameAss = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "Assembly-CSharp");
- #endif
-
- //这个地方必须要有个物体,提供给游戏作为入口
- AssetBundle prefabAb = AssetBundle.LoadFromMemory(GetAssetData("prefabs"));
- GameObject testPrefab = Instantiate(prefabAb.LoadAsset
("HotUpdatePrefab.prefab")); -
- }
-
-
-
- ///
- /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
- /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
- ///
- private static void LoadMetadataForAOTAssemblies()
- {
- // 可以加载任意aot assembly的对应的dll。但要求dll必须与unity build过程中生成的裁剪后的dll一致,而不能直接使用原始dll。
- // 我们在BuildProcessors里添加了处理代码,这些裁剪后的dll在打包时自动被复制到 {项目目录}/HybridCLRData/AssembliesPostIl2CppStrip/{Target} 目录。
-
- /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
- /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
- ///
- foreach (var aotDllName in AOTMetaAssemblyNames)
- {
- byte[] dllBytes = GetAssetData(aotDllName);
- // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
- LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes);
- Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");
- }
- }
- }
通过以上脚本,配合将热更资源放到对应服务器,我们可以实现代码的热更。
接着在对应预制体身上绑定如下脚本,从而实现找到游戏中物体并正确启动的目的。
- using HybridCLR;
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
-
- ///
- /// 该脚本提供游戏运行初始化操作,必走
- ///
- public class LoginAwake : MonoBehaviour
- {
- // Start is called before the first frame update
- void Start()
- {
- Debug.Log("可以进行游戏里面的一些启动操作,从而实现加载");
- //这里可以进行游戏里面的一些启动操作,从而实现加载
- //GameObject.Find("Login").GetComponent
().enabled = true; - gameObject.AddComponent
(); - Debug.Log("=======看到此条日志代表你成功运行了示例项目的热更新代码=======");
-
- gameObject.AddComponent
(); -
- }
-
- // Update is called once per frame
- void Update()
- {
-
- }
- }
其中CreateByCode是官方提供的测试脚本,Login是我们自己写的初始化脚本。这里需要注意的是,启动的脚本貌似必须得继承HybridCLR,不然可能因为解释器的原因不能正确执行(可能是因为解释器启动前,不支持两套脚本读取方式)
接着就是我们的Login脚本,这里可以做我们自己的操作,比如说一些ab包热更之类。这里我并没有使用官方加载资源的方式,毕竟使用 HybridCLR只需要解决代码热更就好了。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEngine.UI;
-
-
- public class Login : MonoBehaviour
- {
- public Text loginTxt;
- // Start is called before the first frame update
- void Start()
- {
- Debug.Log("启动加载");
- loginTxt = GameObject.Find("Login/Text").GetComponent
(); - loginTxt.text = "启动:Http加载";
- }
-
- // Update is called once per frame
- void Update()
- {
-
- }
- }
到目前为止,运行截图如下,代表我们已经成功实现了脚本热更:
~~~~~~~~~~~~~~~~~~~~~~~~~以下内容和ab包有关~~~~~~~~~~~~~~~~~~~~~~
另外多闲聊几句,我们的热更资源一般是在分工程里运行的,ab打包工具用的是assetbundle browser,虽然这款软件的作者已经三年没更新了。
博客采用的脚本热更方案在开发阶段基本不用管,打包的时候重新构建下就行,其实相对还比较方案吧,目前没有用到lua热更方案的项目可以考虑采用。
有问题可以私信留言,大家共同交流进步。