HybridCLR的推广已经做得相当好了,不再多做介绍,可以点击进入HybridCLR开源地址了解详情。
在此之前用过tolua和xlua热更框架, 因为C#开发方式实在太爽,想支持热更又不想使用弱类型语言,于是对ILRuntime和HybridCLR进行了评估,最终确定了HybridCLR方案,尽管它还比较新。选它的原因很简单,它最接近于原生C#开发方式,虽然性能相比原生AOT还有一定差距,但是与其它热更方案相比优势明显!
下面记录一下Windows Unity2021.3.7f1环境下UGF + HybridCLR热更接入过程。
HybridCLR目前还不是以Unity插件的形式提供,需要下载hybridclr_trial示例工程提取提供的Dll生成工具和桥接函数生成工具, 以及HybridCLR Runtime接口。
首先找到hybridclr_trial\HybridCLRData\init_local_il2cpp_data.bat, 以文本方式打开,并做如下配置。
@echo off
set IL2CPP_BRANCH=2021.3.1 //这里Unity2020.3.x填2020.3.33; Unity2021.3.x填2021.3.1
...
...
set IL2CPP_PATH=C:\Program Files\Unity\Hub\Editor\2021.3.6f1\Editor\Data\il2cpp //填自己Unity安装目录下的il2cpp文件夹所在路径
......
目前HybridCLR支持的Unity版本为Unity2020.3系列和2021.3系列,即Unity2020.3.x使用il2cpp 2020.3.33分支,Unity2021.3.x使用il2cpp 2021.3.1分支。然后把IL2CPP_PATH配置为自己Unity安装目录下il2cpp所在路径。
修改完成后右键以管理员方式运行init_local_il2cpp_data.bat,win11下先右键打开Windows PowerShell终端,然后在终端中运行。.bat会自动从git下载hybridclr和魔改后支持dll动态加载的il2cpp库,并将IL2CPP_PATH配置的Unity原生il2cpp库与魔改后的il2cpp文件合并。Unity使用魔改过的il2cpp编译也就从底层支持了C#热更新。
为什么要分离程序集?
正如你不能自己把自己举起来,所以需要一个起到桥梁作用的内置程序负责初始化一些比较靠前的逻辑,比如启动游戏后需要先把热更新逻辑Hotfix.dll从资源服务器下载下来,并加载程序集。加载完成后通过反射调用Hotfix.dll中的入口函数切换到热更新逻辑。
程序集拆分为Builtin.Runtime和Hitfix,即内置程序集和热更新程序集两个即可,把内置程序和热更新程序分离到不同的文件夹,在两个文件夹下创建分别创建Assembly Definition. 然后将热更新程序集引用内置程序集。
由于Hotfix程序集是以热更资源的形式存在,不会被编译成.so进入安装包,所以Builtin程序集不能直接调用Hotfix程序集,否则编译时会报错。这就需要一个HotfixEntry作为进入热更逻辑的入口,使用反射的方式进入Hotfix程序集。
为了尽可能把逻辑放到热更新以达到更大可控性,Builtin程序集只处理比较靠前的逻辑,比如更新资源和热更dll,初始化热更dll. 热更dll初始化完成后就进入热更程序集把所有逻辑交由热更新程序集完成。由于GF不支持动态追加Procedure,所以进入热更程序集流程时需要重新为热更新Procedure创建有限状态机,并切换到热更新流程。在Builtin程序集初始化完热更dll后通过反射调用HotfixEntry.StartHotfixLogic()进入热更新逻辑。
//加载热更新Dll完成,进入热更逻辑
if (loadedProgress >= totalProgress)
{
Log.Info("热更dll加载完成, 开始进入HotfixEntry");
loadedProgress = -1;
#if !DISABLE_HYBRIDCLR
var hotfixDll = GFBuiltin.Hotfix.GetHotfixClass("HotfixEntry");
if (hotfixDll == null)
{
Log.Error("获取热更入口类HotfixEntry失败!");
return;
}
hotfixDll.GetMethod("StartHotfixLogic").Invoke(null, new object[] { true });
#else
HotfixEntry.StartHotfixLogic(false);
#endif
using GameFramework;
using GameFramework.Fsm;
using GameFramework.Procedure;
using UnityGameFramework.Runtime;
///
/// 热更逻辑入口
///
public class HotfixEntry
{
public static void StartHotfixLogic(bool enableHotfix)
{
Log.Info("Hotfix Enable:{0}", enableHotfix);
GFBuiltin.Fsm.DestroyFsm();
var fsmManager = GameFrameworkEntry.GetModule();
var procManager = GameFrameworkEntry.GetModule();
//手动把热更新程序集的流程添加进来
ProcedureBase[] procedures = new ProcedureBase[]
{
new PreloadProcedure(),
new ChangeSceneProcedure(),
new MenuProcedure(),
new GameProcedure(),
new GameOverProcedure()
};
procManager.Initialize(fsmManager, procedures);
procManager.StartProcedure();//默认启动热更新程序集的预加载流程
}
}
0.通过HybridCLR内置工具生成桥接函数(MethodBridge)、生成dll(CompileDll):
1. HybridCLR必须要先Build工程,目的是Build时生成代码裁剪后的dll以供HybridCLR进行AOT元数据补充。
由于il2cpp编译后泛型函数的原始函数体元数据会丢失,无法创建出AOT泛型函数的实例就会导致报错。AOT有泛型共享机制,利用这一特性,我们只需要在内置程序集中实例化的类中添加泛型函数的调用,该泛型函数的元数据就会建立并共享。
HybridCLR已经默认在RefTypes.cs中补充了常用的泛型元数据:
泛型函数在开发中使用非常频繁,自己写的泛型值类型函数必须提前注册到AOT以泛型共享, 如下示例,只需在会实例化的类中添加这些泛型函数调用,RefBuiltinAOT()和RefLitJson()无需有任何地方调用。泛型元数据问题也是目前HybridCLR最大的痛点,相信很快HybridCLR官方就会出一个自动扫描添加泛型函数的工具以提升工作流效率。
#region 提前把热更新使用到的值类型泛型注册到AOT,否则报错
///
/// 注册元数据到AOT
///
void RefBuiltinAOT()
{
var param = RefParams.Acquire();
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.GetValue(default, default);
param.TryGetValue(default, out bool _);
param.TryGetValue(default, out int _);
param.TryGetValue(default, out float _);
param.TryGetValue(default, out double _);
param.TryGetValue(default, out Vector2 _);
param.TryGetValue(default, out Vector3 _);
param.TryGetValue(default, out Vector4 _);
param.TryGetValue(default, out Vector2Int _);
param.TryGetValue(default, out Vector3Int _);
param.TryGetValue(default, out Quaternion _);
param.TryGetValue(default, out Rect _);
param.TryGetValue(default, out Bounds _);
param.TryGetValue(default, out Color _);
param.TryGetValue(default, out Color32 _);
}
void RefLitJson()
{
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
LitJson.JsonMapper.RegisterExporter(default);
}
#endregion
2. 把Build后生成的Strip(裁剪)过的dll打包成AssetBundle
Build后HybridCLR会将Strip后的dll复制到项目根目录的HybridCLRData\AssembliesPostIl2CppStrip下。
可以写个工具监听Build进程,Build完成后自动把Strip后的dll复制到Assets指定目录,并修改为Unity支持的资源扩展名以供打包AB:
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using HybridCLR;
using System.IO;
using GameFramework;
public class BuildAppListener : IPostprocessBuildWithReport, IPreprocessBuildWithReport, IPostBuildPlayerScriptDLLs
{
public int callbackOrder => 100;
public void OnPostBuildPlayerScriptDLLs(BuildReport report)
{
//Debug.LogFormat("OnPostBuildPlayerScriptDLLs:{0}", report.name);
}
public void OnPostprocessBuild(BuildReport report)
{
Debug.Log("OnPostprocessBuild:");
BuildTarget target = report.summary.platform;
//CompileDllHelper.CompileDll(target);
var hotfixDllDir = UtilityExt.Path.GetCombinePath(Application.dataPath, ConstBuiltin.HOT_FIX_DLL_DIR);
try
{
if (!Directory.Exists(hotfixDllDir))
{
Directory.CreateDirectory(hotfixDllDir);
}
else
{
var dllFils = Directory.GetFiles(hotfixDllDir);
for (int i = dllFils.Length - 1; i >= 0; i--)
{
File.Delete(dllFils[i]);
}
}
CopyHotfixDllTo(target, hotfixDllDir);
}
catch (System.Exception e)
{
Debug.LogErrorFormat("生成热更新dll文件失败:{0}", e.Message);
throw;
}
}
public void OnPreprocessBuild(BuildReport report)
{
Debug.Log("OnPreprocessBuild:");
}
public static void CopyHotfixDllTo(BuildTarget target, string desDir, bool copyAotMeta = true)
{
string hotfixDllSrcDir = BuildConfig.GetHotFixDllsOutputDirByTarget(target);
foreach (var dll in BuildConfig.AllHotUpdateDllNames)
{
string dllPath = UtilityExt.Path.GetCombinePath(hotfixDllSrcDir, dll);
if (File.Exists(dllPath))
{
string dllBytesPath = UtilityExt.Path.GetCombinePath(desDir, Utility.Text.Format("{0}.bytes", dll));
File.Copy(dllPath, dllBytesPath, true);
}
}
if (copyAotMeta)
{
string aotDllDir = BuildConfig.GetAssembliesPostIl2CppStripDir(target);
foreach (var dll in BuildConfig.AOTMetaDlls)
{
string dllPath = UtilityExt.Path.GetCombinePath(aotDllDir, dll);
if (!File.Exists(dllPath))
{
Debug.LogError($"ab中添加AOT补充元数据dll:{dllPath} 时发生错误,文件不存在。裁剪后的AOT dll在BuildPlayer时才能生成,因此需要你先构建一次游戏App后再打包。");
continue;
}
string dllBytesPath = UtilityExt.Path.GetCombinePath(desDir, Utility.Text.Format("{0}.bytes", dll));
File.Copy(dllPath, dllBytesPath, true);
}
}
AssetDatabase.Refresh();
}
}
#endif
3. 把热更dll添加到GF的AB打包工具并打包AB
4. Build项目并测试。
https://github.com/sunsvip/GF_HybridCLR
GF_HybridCLR是已经集成好的GameFramework + HybridCLR开发框架,有着一整套完善的工作流。包含对GameFramework的扩展,数据表生成工具、代码生成工具、热更新工作流,AssetBundle加密解密。并且针对HybridCLR增加了傻瓜式的安装、更新工具,支持一键开关HybridCLR热更新。
使用方法:
下载GF_HybridCLR工程,点击HybridCLR->Setup 自动配置HybridCLR环境,点击HybridCLR->Update一键更新HybridCLR
手机端热更实测: