Unity | HybridCLR 热更新(Windows端)

目录

一、准备工作

1.环境相关

2.Unity中配置

二、热更新

1.创建 HotUpdate 热更新模块

2.安装和配置HybridCLR

3.配置PlayerSettings

4.创建热更新相关脚本

5.打包dll

6.测试热更新

三、官方文档

四、补充

1. 调用非静态成员函数

 2. 官方示例项目

★ LoadDll流程解释


一、准备工作

1.环境相关

  • 安装git环境。
  • Win下需要安装visual studio 2019或更高版本。安装时至少要包含 使用Unity的游戏开发 和 使用c++的游戏开发 组件。
  • 本文涉及到的Unity版本是2022.3.14f1c1。unity模块必须安装 Windows端:Windows Build Support(IL2CPP)或Mac端:Mac Build Support(IL2CPP)

2.Unity中配置

  • 在unity中创建场景main,并配置好脚本ConsoleToScreen.cs,它可以打印日志到屏幕上,方便定位错误。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConsoleToScreen : MonoBehaviour
{
	const int maxLines = 50;
    const int maxLineLength = 120;
    private string _logStr = "";

    private readonly List<string> _lines = new List<string>();

    public int fontSize = 15;

    void OnEnable() { Application.logMessageReceived += Log; }
    void OnDisable() { Application.logMessageReceived -= Log; }


    public void Log(string logString, string stackTrace, LogType type)
    {
        foreach (var line in logString.Split('\n'))
        {
            if (line.Length <= maxLineLength)
            {
                _lines.Add(line);
                continue;
            }
            var lineCount = line.Length / maxLineLength + 1;
            for (int i = 0; i < lineCount; i++)
            {
                if ((i + 1) * maxLineLength <= line.Length)
                {
                    _lines.Add(line.Substring(i * maxLineLength, maxLineLength));
                }
                else
                {
                    _lines.Add(line.Substring(i * maxLineLength, line.Length - i * maxLineLength));
                }
            }
        }
        if (_lines.Count > maxLines)
        {
            _lines.RemoveRange(0, _lines.Count - maxLines);
        }
        _logStr = string.Join("\n", _lines);
    }

    void OnGUI()
    {
        GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity,
           new Vector3(Screen.width / 1200.0f, Screen.height / 800.0f, 1.0f));
        GUI.Label(new Rect(10, 10, 800, 370), _logStr, new GUIStyle() { fontSize = Math.Max(10, fontSize) });
    }

}
  • 在Build Settings中添加main场景到打包场景列表。

二、热更新

1.创建 HotUpdate 热更新模块

  • 创建 Assets/HotUpdate 目录(目录名称不做要求,可随便起)
  • 在HotUpdate 目录下右键 Create/Assembly Definition,创建一个名为HotUpdate(名称不做要求)的程序集模块。

        当自己创建一个新的程序集定义文件(.asmdef)时,该文件所在目录以及其子目录下的所有C#脚本都会被默认包含进这个新的程序集中。但是,如果子目录下有另一个.asmdef文件,则那个子目录将会成为另一个独立的程序集。

2.安装和配置HybridCLR

  • 主菜单中点击Windows/Package Manager打开包管理器。点击Add package from git URL...,填入https://gitee.com/focus-creative-games/hybridclr_unity.git 或 https://github.com/focus-creative-games/hybridclr_unity.git。
  • 打开菜单HybridCLR/Installer..., 点击安装按钮进行安装。安装完成后会在最后打印 安装成功日志。

  • 配置HybridCLR:打开菜单ProjectSetting / HybridCLR Settings, 在Hot Update Assemblies配置项中添加HotUpdate程序集。

3.配置PlayerSettings

  • 如果你用的hybridclr包低于v4.0.0版本,需要关闭增量式GC(Use Incremental GC) 选项
  • Scripting Backend 切换为 IL2CPP
  • Api Compatability Level 切换为 .Net 4.x(Unity 2019-2020) 或 .Net Framework(Unity 2021+)

4.创建热更新相关脚本

  • 创建 Assets/HotUpdate/Hello.cs 文件,该文件用于测试是否热更新:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Hello : MonoBehaviour
{
    public static void Run()
    {
        Debug.Log("Hello, HybridCLR, V1.0.0");
    }
}
  • 创建Assets/LoadDll.cs脚本,用来加载热更新程序集:
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

public class LoadDll : MonoBehaviour
{
    void Start()
    {
        // Editor环境下,HotUpdate.dll.bytes已经被自动加载,不需要加载,重复加载反而会出问题。
#if !UNITY_EDITOR
        Assembly hotUpdateAss = Assembly.Load(File.ReadAllBytes($"{Application.streamingAssetsPath}/HotUpdate.dll.bytes"));
#else
        // Editor下无需加载,直接查找获得HotUpdate程序集
        Assembly hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
#endif
        //通过反射来调用热更新代码
        Type type = hotUpdateAss.GetType("Hello");
        if (type == null)
        {
            Debug.Log("Hello assembly is null");
        }
        else
        {
            type.GetMethod("Run").Invoke(null, null);
        }

    }
}

        HybridCLR是原生运行时实现,因此调用Assembly Assembly.Load(byte[])即可加载热更新程序集。(为了简化演示,我们不通过http服务器下载HotUpdate.dll,而是直接将HotUpdate.dll放到StreamingAssets目录下

5.打包dll

        如果配置正确,Editor运行和打包后运行的效果一样。

  • 运行菜单 HybridCLR/Generate/All 进行必要的生成操作
  • 将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)目录下的HotUpdate.dll复制到Assets/StreamingAssets/HotUpdate.dll.bytes,注意,要加.bytes后缀

  • 打开Build Settings对话框,点击Build And Run,打包并且运行热更新示例工程。

        如果打包成功,并且屏幕上显示 'Hello, HybridCLR, V1.0.0',表示热更新代码被顺利执行!

6.测试热更新

  • 修改Assets/HotUpdate/Hello.cs的Run函数中Debug.Log("Hello, HybridCLR, V1.0.0");代码,改成Debug.Log("Hello, HybridCLR, V1.1.0");。
  • 运行菜单命令HybridCLR/CompileDll/ActiveBulidTarget重新编译热更新代码
  • 将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)目录下的HotUpdate.dll复制为刚才的打包输出目录的 XXX_Data/StreamingAssets/HotUpdate.dll.bytes。
  • 重新运行程序,会发现屏幕中显示Hello, HybridCLR, V1.1.0,表示热更新代码生效了!

三、官方文档

四、补充

1. 调用非静态成员函数

        修改Hello.cs为如下:

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

public class Hello
{
    public static void Run()
    {
        Debug.Log("Hello, HybridCLR, V1.1.0");
    }
    public void TestNonStatic()
    {
        Debug.Log("Hello, HybridCLR, 非静态成员函数");
    }
}

         LoadDll.cs代码如下:

        Type type = hotUpdateAss.GetType("Hello");
        if (type == null)
        {
            Debug.Log("Hello is null");
        }
        else
        {
            type.GetMethod("Run").Invoke(null, null);
            //调用TestNonStatic
            type.GetMethod("TestNonStatic").Invoke(Activator.CreateInstance(type), null);
        }

        GetMethod会返回一个MethodInfo 对象,但不能直接方法一样调用它,因为 MethodInfo 是一个描述方法的元数据对象,而不是方法本身。此时需要用到Invoke方法。

  Invoke方法的第一个参数是要对其调用方法的对象(如果调用静态方法,则为null),第二个参数是一个object[]数组,包含要传递给方法的参数。如果方法不需要参数,则传递null或空数组。 

        需要注意Hello类不可继承自MonoBehaviour,否则会警告:You are trying to create a MonoBehaviour using the 'new' keyword.  This is not allowed.  MonoBehaviours can only be added using AddComponent(). Alternatively, your script can inherit from ScriptableObject or no base class at all.

        因为在Unity中,MonoBehaviour是一种特殊的类,它代表附加到游戏对象上的组件。要创建并添加一个新的MonoBehaviour到一个GameObject上,需要使用AddComponent()方法。

        如果想要创建一个没有任何依赖性或者不需要附加到特定游戏对象上的类,那么将其改为继承自 ScriptableObject 或者没有基类都是可行的。

 2. 官方示例项目

  • HotUpdate:热更新代码模块
  • Main:AOT主包模块,对应常规项目的主项目,资源更新模块

        根据文档进行操作:

 运行结果:

★ LoadDll流程解释

        首先,通过UnityWebRequest加载五个文件,包括热更新模块的2个文件:"prefabs"、 "HotUpdate.dll.bytes" 和AOT主包模块的三个文件:"mscorlib.dll.bytes"、 "System.dll.bytes"、 "System.Core.dll.bytes"。

        然后补充AOT模块的元数据,代码如下:

 private static void LoadMetadataForAOTAssemblies()
    {
        /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
        /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
        /// 
        HomologousImageMode mode = HomologousImageMode.SuperSet;
        foreach (var aotDllName in AOTMetaAssemblyFiles)
        {
            byte[] dllBytes = ReadBytesFromStreamingAssets(aotDllName);
            // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
            LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);//用来加载预先编译的程序集的元数据,这样就可以在运行时为 AOT 泛型函数生成缺失的 native 函数。
            Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
        }
    }

         AOT 编译过程中并不是所有的代码都能被直接转换成本地代码。例如,泛型类型和方法在编译时可能不会全部实例化,因为它们的具体类型可能直到运行时才能确定。
        因此,即使 DLL 文件中包含了元数据,HybridCLR 等运行时环境在执行 AOT 编译的程序集时,仍然需要额外的步骤来加载这些元数据。这样做的目的是为了:

  • 动态类型实例化:运行时可以使用元数据来动态地实例化泛型类型或方法,即使它们在 AOT 编译过程中没有被实例化。
  • 反射支持:运行时可以使用元数据来支持反射操作,允许程序在运行时查询和操作类型信息。
  • 完整性和安全性:确保运行时环境能够访问所有必要的类型信息,以保证程序的正常运行和安全性。

        RuntimeApi.LoadMetadataForAOTAssembly 函数的作用就是在运行时加载这些必要的元数据,确保即使在 AOT 环境中,程序也能够正常使用泛型和反射等功能。

        最后调用热更新模块中Entry类中的Start函数,并实例化Prefab:

    void StartGame()
    {
        LoadMetadataForAOTAssemblies();
#if !UNITY_EDITOR
        _hotUpdateAss = Assembly.Load(ReadBytesFromStreamingAssets("HotUpdate.dll.bytes"));
#else
        _hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
#endif
        Type entryType = _hotUpdateAss.GetType("Entry");
        entryType.GetMethod("Start").Invoke(null, null);

        Run_InstantiateComponentByAsset();
    }

    private static void Run_InstantiateComponentByAsset()
    {
        // 通过实例化assetbundle中的资源,还原资源上的热更新脚本
        AssetBundle ab = AssetBundle.LoadFromMemory(LoadDll.ReadBytesFromStreamingAssets("prefabs"));
        GameObject cube = ab.LoadAsset<GameObject>("Cube");
        GameObject.Instantiate(cube);
    }
_hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");

        这行代码用于从当前应用程序域中检索一个名为 "HotUpdate" 的程序集。

  • System.AppDomain.CurrentDomain:这部分引用了当前线程正在运行的应用程序域。在 .NET 中,应用程序域(AppDomain)是一个隔离的环境,用于运行应用程序代码,一个进程可以包含多个应用程序域。

  • GetAssemblies():这是 AppDomain 类的一个方法,返回当前应用程序域中已加载的程序集数组。程序集是包含代码和资源的单元,通常是以 DLL 或 EXE 形式存在。

  • First(a => a.GetName().Name == "HotUpdate")这是一个 LINQ 查询表达式,它使用 First 方法来遍历程序集数组,并查找第一个其名称匹配 "HotUpdate" 的程序集。a 是一个范围变量,代表数组中的一个程序集。a.GetName() 获取程序集的名称,a.GetName().Name 获取程序集名称的字符串表示。如果没有找到任何匹配的程序集,First 方法将抛出一个异常。

  • _hotUpdateAss这个变量用于存储找到的程序集。