Unity 3D模型展示框架篇之Addressables+ILRuntime热更(完结篇)

系列文章目录

Unity 3D模型展示框架篇之项目整理
Unity 3D模型展示框架篇之框架运用
Unity 3D模型展示框架篇之自由观察(Cinemachine)
Unity 3D模型展示框架篇之资源打包、加载、热更(Addressable Asset System | 简称AA)
Unity 3D模型展示框架篇之资源打包、加载、热更(二)
Unity 3D模型展示框架篇之ILRuntime快速入门
Unity 3D模型展示框架篇之ILRuntime整合与应用



前言

本项目将整合之前Unity程序基础小框架专栏在Unity3D模型展示项目基础上进行整合,并记录了集成过程中对原脚本的调整过程。增加了Asset Bundle+ILRuntime热更新技术流程。


本文章主要介绍如何在Unity工程中使用Addressable与ILRuntime项目进行整合的过程与使用实例。

一、热更新为什么要Addressables+ILRuntime?

ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新。
Addressables是官方推出的可寻址资产系统提供了一种通过"地址"加载资产的简单方法。它通过简化内容包的创建和部署来处理资产管理开销。可寻址资产系统使用异步加载来支持从任何位置加载任何依赖项集合。无论您使用直接引用、传统的资产包还是ResourceFolders进行资产管理,可寻址资产都提供了一种更简单的方法来使您的游戏更加动态。什么是资产?资产是您用来创建游戏或应用程序的内容。资产的常见例子包括预制、纹理、材料、音频剪辑和动画。什么是可寻址资产?将资产设置为“可寻址”允许您使用该资产的唯一地址在任何地方调用它。

简单来说ILRuntime+Addressables涵盖了热更新中的代码更新与资源热更新。

ILRuntime详见官方文档
Addressables官方文档

二、整合步骤

由于Addressables不支持DLL资源打包,因此需要将DLL文件转化为二进制文件,再通过Addressables进行打包进行资源热更后,加载时通过DLL文件流初始化ILRuntime的入口应用程序域AppDomain。完成对ILRuntime代码工程的更新。

1.ILRuntime工程打包DLL转化为二进制文件

  • 修改ILRuntime热更工程的DLL输出目录
    修改目录的原因是因为StreamingAssets在进行Addressables分组时报错。因此创建Assets/ILRunTimeAndAddressable/AssetsPackage/HotFixDllAssets/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes,HotFixDll为生成DLL文件目录,HotFixDllToBytes为DLL转化二进制文件的存放目录。
    在这里插入图片描述
    在这里插入图片描述

  • 创建DLL转二进制文件编辑器
    在Editor文件夹中创建DllToBytes.cs
    在这里插入图片描述

DllToBytes.cs代码如下:


using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;


public class DllToBytes 
{
    public static string normalPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
    public static string normalPathToSave = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes";
    [MenuItem("MyMenu/ILRuntime/DllToByte")]
    public static void DllToByte()
    {
        DllToByte(true);
    }
    [MenuItem("MyMenu/ILRuntime/DllToByteChoose")]
    public static void DllToByteChoose()
    {
        DllToByte(false);
    }

    private static void DllToByte(bool autoChoosePath)
    {
        string folderPath, savePath;
        if (autoChoosePath)
        {
            folderPath = normalPath;
        }
        else
        {
            folderPath = EditorUtility.OpenFolderPanel("dll所在的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
        }

        if (string.IsNullOrEmpty(folderPath))
        {
            return;
        }
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
        FileInfo[] fileInfos = directoryInfo.GetFiles();
        List<FileInfo> listDll = new List<FileInfo>();
        List<FileInfo> listPdb = new List<FileInfo>();

        for (int i = 0; i < fileInfos.Length; i++)
        {
            if (fileInfos[i].Extension == ".dll")
            {
                listDll.Add(fileInfos[i]);
            }

            else if (fileInfos[i].Extension == ".pdb")
            {
                listPdb.Add(fileInfos[i]);
            }
        }

        if (listDll.Count + listPdb.Count <= 0)
        {
            Debug.Log("文件夹下没有dll文件");
        }
        else
        {
            Debug.Log("路径为:" + folderPath);
        }

        if (autoChoosePath)
        {
            savePath = normalPathToSave;
        }
        else
        {
            savePath = EditorUtility.OpenFolderPanel("dll要保存的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
        }
        Debug.Log("-----开始转换dll文件------");
        string path = string.Empty;
        for (int i = 0; i < listDll.Count; i++)
        {
            //$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了

            path = $"{savePath}/{Path.GetFileNameWithoutExtension(listDll[i].Name)}_dll_res.bytes";
            Debug.Log(path);
            BytesToFile(path, FileToBytes(listDll[i]));
        }
        Debug.Log("------dll文件转换结束---------");

        Debug.Log("-----开始转换pdb文件------");
        for (int i = 0; i < listPdb.Count; i++)
        {
            //$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了

            path = $"{savePath}/{Path.GetFileNameWithoutExtension(listPdb[i].Name)}_pdb_res.bytes";
            BytesToFile(path, FileToBytes(listPdb[i]));
        }
        Debug.Log("------pdb文件转换结束---------");
        AssetDatabase.Refresh();

    }
    /// <summary>
    /// file转byte
    /// </summary>
    /// <param name="fileInfo"></param>
    /// <returns></returns>
    private static byte[] FileToBytes(FileInfo fileInfo)
    {
        return File.ReadAllBytes(fileInfo.FullName);
    }
    /// <summary>
    /// byte转文件
    /// </summary>
    /// <param name="path"></param>
    /// <param name="bytes"></param>
    private static void BytesToFile(string path, byte[] bytes)
    {
        Debug.Log($"路径为:{path}\nlength:{bytes.Length}");
        File.WriteAllBytes(path, bytes);
    }

}

在这里插入图片描述
在这里插入图片描述

2.Unity工程加载热更DLL二进制文件

HotFixMgr.cs 增加LoadHotFixAssembly通过二进制文件加载热更DLL。修改之前加载DLL的路径Application.streamingAssetsPath改为新改的路径。整体代码如下:
在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class HotFixMgr : MonoBehaviour
{
    public  static HotFixMgr instance;
    public ILRuntime.Runtime.Enviorment.AppDomain appdomain;
    private System.IO.MemoryStream m_fs, m_p;
    public static HotFixMgr Instance
    {
        get
        {
            if (instance==null)
            {
                instance=new GameObject("HotFixMgr").AddComponent<HotFixMgr>();
                instance.LoadHotFixAssembly();
            }
            return instance;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
    }
        public void LoadHotFixAssembly() {
        dllPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
#if UNITY_ANDROID
    WWW www = new WWW(Application.streamingAssetsPath + "/Hotfix.dll");
#else
        WWW www = new WWW("file:///" + dllPath + "/HotFix_Project.dll");
#endif
        while (!www.isDone)
            //yield return null;
            System.Threading.Thread.Sleep(100);
        if (!string.IsNullOrEmpty(www.error))
            Debug.LogError(www.error);
        byte[] dll = www.bytes;
        www.Dispose();
#if UNITY_ANDROID
    www = new WWW(Application.streamingAssetsPath + "/Hotfix.pdb");
#else
        www = new WWW("file:///" + dllPath + "/HotFix_Project.pdb");
#endif
        while (!www.isDone)
            //yield return null;
            System.Threading.Thread.Sleep(100);
        if (!string.IsNullOrEmpty(www.error))
            Debug.LogError(www.error);
        byte[] pdb = www.bytes;
        System.IO.MemoryStream fs = new MemoryStream(dll);
        System.IO.MemoryStream p = new MemoryStream(pdb);
        appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());

        OnILRuntimeInitialized();
    }


    /// <summary>
    /// 加载dll,pdb
    /// </summary>
    /// <param name="dll"></param>
    /// <param name="pdb"></param>
    public void LoadHotFixAssembly(byte[] dll, byte[] pdb)
    {
        m_fs = new MemoryStream(dll);
        m_p = new MemoryStream(pdb);
        try
        {
            appdomain.LoadAssembly(m_fs, m_p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
            return;
        }
        appdomain.DebugService.StartDebugService(56000);
        OnILRuntimeInitialized();
    }

    void OnILRuntimeInitialized()
    {
        //appdomain.Invoke("Hotfix.Game", "Initialize", null, null);

#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif

        //下面再举一个这个Demo中没有用到,但是UGUI经常遇到的一个委托,例如UnityAction<float>
        appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((action) =>
        {
            return new UnityEngine.Events.UnityAction(() =>
            {
                ((System.Action)action)();
            });
        });
    }
    // Update is called once per frame
    void Update()
    {

    }
}

修改ProLaunch.cs增加对热更DLL的加载入口
在这里插入图片描述
具体代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
/// <summary>
/// 加载方式
/// </summary>
public enum LoadMode
{
    ByLocalDll,
    ByLocalAddressable
}


public class ProLaunch : MonoBehaviour
{

    /// <summary>
    /// 显示下载状态和进度
    /// </summary>
    public Text UpdateText;
    public Text DownText;

    public Button btnCheckAndUpdate;
    public Button btnUpdate;
    public Button btnDown;
    public Button btnLogin;
    public Slider Slider;//滑动条组件

    private List<object> _updateKeys = new List<object>();

    //public Transform father;
    [Tooltip("dll文件的加载方式")]
    public LoadMode loadingMode = LoadMode.ByLocalAddressable;
    // Start is called before the first frame update
    void Start()
    {
        //retryBtn.gameObject.SetActive(false);
        btnCheckAndUpdate.onClick.AddListener(() =>
        {
            StartCoroutine(DoUpdateAddressadble());
        });
        btnUpdate.onClick.AddListener(() =>
        {
            UpdateCatalog();
        });
        // 默认自动执行一次更新检测
        //StartCoroutine(DoUpdateAddressadble());
        StartHotFix();
        btnDown.onClick.AddListener(() =>
        {
            DownLoad();
        });



        btnLogin.onClick.AddListener(() =>
        {
            SceneManager.LoadScene(1);
            //StartCoroutine(LoadScene("Test2"));

        });
    }

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

    /// <summary>
    /// 加载dll
    /// </summary>
    /// <returns></returns>
    public async System.Threading.Tasks.Task StartHotFix()
    {

        //去服务器上下载最新的aa包资源

        //下载热更代码
        //string m_url=null;
        byte[] dll = new byte[] { };
        byte[] pdb = new byte[] { };
        if (loadingMode == LoadMode.ByLocalDll)
        {
            //StartCoroutine(CheckHotUpdate(dll,pdb));

           HotFixMgr.instance.LoadHotFixAssembly();
        }
        else if (loadingMode == LoadMode.ByLocalAddressable)
        {
            TextAsset assetDll = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_dll_res").Task;
            dll = assetDll.bytes;
            TextAsset assetPdb = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_pdb_res").Task;
            pdb = assetPdb.bytes;
            HotFixMgr.instance.LoadHotFixAssembly(dll, pdb);


        }

    }
    public async void UpdateCatalog()
    {
        //初始化Addressable
        var init = Addressables.InitializeAsync();
        await init.Task;

        //开始连接服务器检查更新
        var handle = Addressables.CheckForCatalogUpdates(false);
        await handle.Task;
        Debug.Log("check catalog status " + handle.Status);
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            List<string> catalogs = handle.Result;
            if (catalogs != null && catalogs.Count > 0)
            {
                foreach (var catalog in catalogs)
                {
                    Debug.Log("catalog  " + catalog);
                }
                Debug.Log("download catalog start ");

                UpdateText.text = UpdateText.text + "\n下载更新catalog";
                var updateHandle = Addressables.UpdateCatalogs(catalogs, false);
                await updateHandle.Task;
                foreach (var item in updateHandle.Result)
                {
                    Debug.Log("catalog result " + item.LocatorId);

                    foreach (var key in item.Keys)
                    {
                        Debug.Log("catalog key " + key);
                    }
                    _updateKeys.AddRange(item.Keys);
                }
                Debug.Log("download catalog finish " + updateHandle.Status);

                UpdateText.text = UpdateText.text + "\n更新catalog完成" + updateHandle.Status;
            }
            else
            {
                Debug.Log("dont need update catalogs");
                UpdateText.text = "没有需要更新的catalogs信息";
            }
        }
        Addressables.Release(handle);

    }


    public void DownLoad()
    {
        StartCoroutine(DownAssetImpl());
    }

    public IEnumerator DownAssetImpl()
    {
        var downloadsize = Addressables.GetDownloadSizeAsync(_updateKeys);
        yield return downloadsize;
        Debug.Log("start download size :" + downloadsize.Result);
        UpdateText.text = UpdateText.text + "\n更新文件大小" + downloadsize.Result;

        if (downloadsize.Result > 0)
        {
            var download = Addressables.DownloadDependenciesAsync(_updateKeys, Addressables.MergeMode.Union);
            yield return download;
            //await download.Task;
            Debug.Log("download result type " + download.Result.GetType());
            UpdateText.text = UpdateText.text + "\n下载结果类型 " + download.Result.GetType();


            foreach (var item in download.Result as List<UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource>)
            {
                var ab = item.GetAssetBundle();
                Debug.Log("ab name " + ab.name);

                UpdateText.text = UpdateText.text + "\n ab名称 " + ab.name;


                foreach (var name in ab.GetAllAssetNames())
                {
                    Debug.Log("asset name " + name);
                    UpdateText.text = UpdateText.text + "\n asset 名称 " + name;

                }
            }
            Addressables.Release(download);
        }
        Addressables.Release(downloadsize);
    }


    IEnumerator LoadScene(string senceName)
    {
        // 异步加载场景(如果场景资源没有下载,会自动下载),

        var handle = Addressables.LoadSceneAsync(senceName);
        if (handle.Status == AsyncOperationStatus.Failed)
        {
            Debug.LogError("场景加载异常: " + handle.OperationException.ToString());
            yield break;
        }
        while (!handle.IsDone)
        {
            // 进度(0~1)
            float percentage = handle.PercentComplete;
            Debug.Log("进度: " + percentage);
            yield return null;
        }

        Debug.Log("场景加载完毕");
    }


    IEnumerator DoUpdateAddressadble()
    {
        AsyncOperationHandle<IResourceLocator> initHandle = Addressables.InitializeAsync();
        yield return initHandle;

        // 检测更新
        var checkHandle = Addressables.CheckForCatalogUpdates(false);
        yield return checkHandle;
        if (checkHandle.Status != AsyncOperationStatus.Succeeded)
        {
            OnError("CheckForCatalogUpdates Error\n" + checkHandle.OperationException.ToString());
            yield break;
        }

        if (checkHandle.Result.Count > 0)
        {
            var updateHandle = Addressables.UpdateCatalogs(checkHandle.Result, false);
            yield return updateHandle;

            if (updateHandle.Status != AsyncOperationStatus.Succeeded)
            {
                OnError("UpdateCatalogs Error\n" + updateHandle.OperationException.ToString());
                yield break;
            }

            // 更新列表迭代器
            List<IResourceLocator> locators = updateHandle.Result;
            foreach (var locator in locators)
            {
                List<object> keys = new List<object>();
                keys.AddRange(locator.Keys);
                // 获取待下载的文件总大小
                var sizeHandle = Addressables.GetDownloadSizeAsync(keys);
                yield return sizeHandle;
                if (sizeHandle.Status != AsyncOperationStatus.Succeeded)
                {
                    OnError("GetDownloadSizeAsync Error\n" + sizeHandle.OperationException.ToString());
                    yield break;
                }

                long totalDownloadSize = sizeHandle.Result;
                UpdateText.text = UpdateText.text + "\ndownload size : " + totalDownloadSize;
                Debug.Log("download size : " + totalDownloadSize);
                if (totalDownloadSize > 0)
                {
                    // 下载
                    var downloadHandle = Addressables.DownloadDependenciesAsync(keys, Addressables.MergeMode.Union, false);
                    //yield return downloadHandle;
                    while (!downloadHandle.IsDone)
                    {
                        if (downloadHandle.Status == AsyncOperationStatus.Failed)
                        {
                            OnError("DownloadDependenciesAsync Error\n" + downloadHandle.OperationException.ToString());
                            yield break;
                        }
                        // 下载进度
                        float percentage = downloadHandle.PercentComplete;


                        Debug.Log($"已下载: {percentage}");
                        DownText.text = $"已下载: {Mathf.Round(percentage * 100)}%";
                        Slider.value = percentage;
                        if (percentage >= 0.98f)//如果进度条已经到达90%
                        {
                          
                            Slider.value = 1; //那就让进度条的值编变成1
                        }

                        yield return null;
                    }
                    yield return downloadHandle;
                    if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
                    {
                        Debug.Log("下载完毕!");
                        DownText.text = DownText.text + " 下载完毕";
                    }
                }
            }
        }
        else
        {
            UpdateText.text = UpdateText.text + "\n没有检测到更新";
        }

        // 进入游戏
        EnterPro();
    }
    // 进入游戏
    void EnterPro()
    {
        // TODO
        UpdateText.text = UpdateText.text + "\n进入游戏场景";
        Debug.Log("进入游戏");
    }
    private void OnError(string msg)
    {
        UpdateText.text = UpdateText.text + $"\n{msg}\n请重试! ";
    }
}

3.工程整合与测试

测试:在预制体LoginUI中新增退出按钮,并在热更工程中创建绑定按钮单击的注册事件。使用Addressables进行分组,构建更新包进行测试。具体步骤如下:

  • 修改热更工程中的HotFix.cs,增加注册按钮单击事件静态方法。代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

namespace HotFix_Project
{
    public class HotFix
    {
        void Awake()
        {
            UnityEngine.Debug.Log("Awake");
        }
        public  static void HelloWorld(string name,int age) {
            UnityEngine.Debug.Log("HelloWorld "+name+" "+ age);
        }

        public string Hello(string name) {
            return "hello " + name;
        }
        public static void bindClick(GameObject btn) {
            btn.transform.GetComponent<Button>().onClick.AddListener(()=> {
                UnityEngine.Debug.Log("Hot click");
            });
        }
    }
}

  • 修改预制体LoginUI增加退出按钮,创建login.cs并调用热更工程中的代码进行注册,在loginUI中挂在此脚本。代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Login : MonoBehaviour
{
    public GameObject btn;

    // Start is called before the first frame update
    void Start()
    {
        string className = "HotFix_Project.HotFix";
        string funName = "bindClick";
        btn = gameObject.transform.Find("btnClose").gameObject;
        HotFixMgr.Instance.appdomain.Invoke(className, funName, null, btn);
    }
    // Update is called once per frame
    void Update()
    {
    }
}

  • 本地进行测试

在这里插入图片描述
运行代码,点击退出按钮后:
在这里插入图片描述

  • 构建热更新包并进行发布
    在这里插入图片描述
    修改启动程序中加载DLL的方式改为如图所示:
    在这里插入图片描述

进行构建并将更新的文件Copy到服务器上。
在这里插入图片描述
修改Play Mode Script如图进行设置
在这里插入图片描述

4.效果演示

Unity 3D模型展示框架篇效果演示


总结

以上就是今天要讲的内容,本文介绍如何通过ILRuntime+Adressables技术完成热更案例的整合与测试,这也是Unity 3D模型展示框架篇系列文章的最后一篇,在阅读过程中发现存在遗漏或不足欢迎大家留言指正。


点赞过千公布项目案例
在这里插入图片描述

  • 19
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yxlalm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值