jjzjj

Unity3D热更设计:一款基于 HybridCLR的C#热更方案

鹿野素材屋 2024-03-26 原文

  在这篇文章之前,可以转到我的这两篇博客: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";//一些基本的网络配置

}

/// <summary>
/// 初始化加载dll脚本:这里如果需要的话,最好是把表格数据加载也放到最开始
/// </summary>
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 热更部分
    /// <summary>
    /// 进度
    /// </summary>
    public float progress;
    /// <summary>
    /// 版本更新       
    /// </summary>
    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<BundleInfo> bims = new List<BundleInfo>();
            FileMD5 date = JsonUtility.FromJson<FileMD5>(_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));
            }

        }
    }

    /// <summary>
    /// 删除所有不受版本控制的所有文件
    /// </summary>
    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;
    }

    /// <summary>
    /// 校正MD5 list
    /// </summary>
    public List<MD5Message> ReviseMD5List(MD5Message[] _list)
    {
        var _checkList = new List<MD5Message>();
        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;
    }

    /// <summary>
    /// 路径比对
    /// </summary>  
    public IEnumerator DownLoadBundleFiles(List<BundleInfo> 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);
    }


    /// <summary>
    /// 记录信息
    /// </summary>
    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();
        }
    }


    /// <summary>
    /// 获得下载的URL
    /// </summary>
    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<GameObject>("HotUpdatePrefab.prefab"));

    }



    /// <summary>
    /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
    /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
    /// </summary>
    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;


/// <summary>
/// 该脚本提供游戏运行初始化操作,必走
/// </summary>
public class LoginAwake : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("可以进行游戏里面的一些启动操作,从而实现加载");
        //这里可以进行游戏里面的一些启动操作,从而实现加载
        //GameObject.Find("Login").GetComponent<Login>().enabled = true;
        gameObject.AddComponent<CreateByCode>();
        Debug.Log("=======看到此条日志代表你成功运行了示例项目的热更新代码=======");

        gameObject.AddComponent<Login>();

    }

    // 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<Text>();
        loginTxt.text = "启动:Http加载";
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

  到目前为止,运行截图如下,代表我们已经成功实现了脚本热更:

~~~~~~~~~~~~~~~~~~~~~~~~~以下内容和ab包有关~~~~~~~~~~~~~~~~~~~~~~

  另外多闲聊几句,我们的热更资源一般是在分工程里运行的,ab打包工具用的是assetbundle browser,虽然这款软件的作者已经三年没更新了。

  博客采用的脚本热更方案在开发阶段基本不用管,打包的时候重新构建下就行,其实相对还比较方案吧,目前没有用到lua热更方案的项目可以考虑采用。

  有问题可以私信留言,大家共同交流进步。

有关Unity3D热更设计:一款基于 HybridCLR的C#热更方案的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  3. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  4. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. unity---接入Admob - 2

    目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里​编辑 3.解析依赖到项目中

  9. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  10. [Vuforia]二.3D物体识别 - 2

    之前说过10之后的版本没有3dScan了,所以还是9.8的版本或者之前更早的版本。 3d物体扫描需要先下载扫描的APK进行扫面。首先要在手机上装一个扫描程序,扫描现实中的三维物体,然后上传高通官网,在下载成UnityPackage类型让Unity能够使用这个扫描程序可以从高通官网上进行下载,是一个安卓程序。点到Tools往下滑,找到VuforiaObjectScanner下载后解压数据线连接手机,将apk文件拷入手机安装然后刚才解压文件中的Media文件夹打开,两个PDF图打印第一张A4-ObjectScanningTarget.pdf,主要是用来辅助扫描的。好了,接下来就是扫描三维物体。将瓶

随机推荐