Hybird热更预研汇总(0.8.0)
(代号wolong)是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
HybridCLR扩充了il2cpp的代码,使它由纯runtime变成‘AOT+Interpreter’ 混合runtime,进而原生支持动态加载assembly,使得基于il2cpp backend打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行。从底层彻底支持了热更新。
注释:
AOT,Ahead Of Time,指运行前编译。JIT,即Just-in-time,动态(即时)编译,边运行边编译。
IL2cpp:Unity公司就自行研发了IL2cpp,把本来应该再mono的虚拟机上跑的中间代码转换成cpp代码,这样再把生成的cpp代码,利用c++的跨平台特性,在各个平台上通过对各平台都有良好优化的native c++编译器编译,以获得更高的效率和更好的兼容性。
特性。
1.特性完整,近乎完整实现了。除了 下文中"限制和注意事项" 之外的特性都支持。
2.零学习和使用成本。 HybridCLR将纯AOT runtime增强为完整的runtime,使得热更新代码与AOT代码无缝工作。脚本类与AOT类在同一个运行时内,可以随意写继承、反射、多线程(volatile、ThreadStatic、Task、async)之类的代码。不需要额外写任何特殊代码、没有代码生成,也没有什么特殊限制。
3.执行高效。实现了一个极其高效的寄存器解释器,所有指标都大幅优于其他热更新方案
4.内存高效。 热更新脚本中定义的类跟普通c#类占用一样的内存空间,远优于其他热更新方案。
5.原生支持hotfix修复AOT部分代码。几乎不增加任何开发和运行开销。
限制和注意事项:(后面补充)
与其他流行的c#热更新方案的区别
HybridCLR是原生的c#热更新方案。通俗地说,il2cpp相当于mono的aot模块,HybridCLR相当于mono的interpreter模块,两者合一成为完整mono。HybridCLR使得il2cpp变成一个全功能的runtime,原生(即通过System.Reflection.Assembly.Load)支持动态加载dll,从而支持ios平台的热更新。
其他热更新方案则是独立vm,与il2cpp的关系本质上相当于mono中嵌入lua的关系。因此类型系统不统一,为了让热更新类型能够继承AOT部分类型,需要写适配器,并且解释器中的类型不能为主工程的类型系统所识别。特性不完整、开发麻烦、运行效率低下。
稳定性状况
目前PC(x86及x64)、macOS(x86、x64、Arm64)、Android(arm v7及v8)、iOS(64bit) 可稳定使用。已经有数百个大中小型商业游戏项目完成接入,其中一些在紧锣密鼓作上线前测试。
Hybrid导入
- 打开PackageManager 通过 Add package from git URL 添加 或者下载到本地, 通过 Add package from disk 导入插件。
- HybridCLR ---- Installer 导入安装 (当前项目2020.3.29f1可直接点击安装,更低版本需要手动操作)
3.PlayerSetting配置
- 关闭增量式GC(Use Incremental GC) 选项。因为目前不支持增量式GC
- Scripting Backend 切换为 il2cpp
- Api Compatability Level 切换为 .Net 4 or .Net Framework (打主包时可以使用.net standard,但使用脚本Compile热更新dll时必须切换到.Net 4.x or .Net Framework
4.热更新模块拆分
很显然,项目必须拆分为AOT(即编译到游戏主包内)和热更新 assembly,才能进行热更新。HybridCLR对于 怎么拆分程序集并无任何限制,甚至你将AOT或者热更新程序集放到第三方工程中也是可以的。
常见的拆分方式有几种:
- 使用Unity支持的Assembly Definition将整个项目拆分为多个程序集,Assembly-CSharp作为AOT程序集,不引用任何热更新程序集的代码
- 将AOT部分拆分为一个或多个程序集,Assembly-CSharp作为热更新程序集,同时还有其他0-N个热更新程序集。
无论哪种拆分方式,正确设置好程序集之间的引用关系即可。
点击菜单 HybridCLR/Settings 打开配置界面。
对于项目中的热更新程序集,如果是assembly definition(asmdef)定义的程序集,加入 hotUpdateAssemblyDefinitions列表,如果是普通dll,则将程序集名字(不包含'.dll'后缀,如Main、Assembly-CSharp)加入hotUpdateAssemblies即可。这两个列表是等价的,不要重复添加,否则会报错。
Hybrid - PC平台测试
1.安装Addressables Window - PackageManager - Package:Unity Registry - 找到Addressables
2.安装Python2.7
3.VS安装C++开发支持 (VS - 工具 - 获取工具和功能)
4.Gradle5.6.4 升级到Gradle 6.1.1
2020.3.29版本测试(新建测试工程Hybrid_Kane,仿照示例工程进行设置)
HybridCLR - Build - Win64 打包成功后,运行成功!
新增热更代码Debug.Log("我热更了一句代码")!
HybridCLR - BuildBundles - BuildAll_ActiveBuildTarget
HybridCLR - Build - BuildAssetsAndCopyToStreamingAssets
手动拷贝编辑器工程StreamingAssets文件到打包后工程StreamingAssets
可见"我热更了一句代码" 至此PC版本测试成功!
Hybrid - Android测试
出包测试:
- 新建
Main.dll 以及包含的LoadDll.cs负责加载热更dll以及aot-dll.
LoadDll 逻辑 加载dll - aot加载原始metadata - 加载热更dll。(基础模块,不能热更)
- 新建Editor - HybridCLRBuilder 负责打包 热更dll以及aot-dll流程处理。
HybridCLRBuilder 逻辑 - 同步热更dll - 打包生成裁剪dll - 清空临时文件夹 - Hybrid 代码Generate生成 - 编写对应平台dll - 打包hybrid特殊ab - 拷贝aot和hotupdate的dll.
- Unity出包Build , 生成apk ,运行成功!
热更测试(接入Dev出包流程):
- LoadDll后执行项目第一个启动文件 (通过加载 hybrid的ab XXX.prefab)
(1)加载逻辑,先读外部目录(persistentDataPath),不存在的话,读内部目录(streamingAssetsPath)
- 打包 在“构建资源AssetBundle”后
- 执行 HybridCLRBuilder的流程,资源根据项目放到basic文件夹下
- 内置资源处理
- 将hybrid特殊ab 和 aot 以及hotupdate的dll 添加到内置资源中。
检查引用丢失(注意事项):
Hybrid与XLua交互:
1.在打包时,先生成wrapper文件,再将文件生成到热更新模块中。
2.因为xlua生成的代码全在全局的Assembly-CSharp里,甚至做成partial类与Runtime代码关联,因此需要做代码 调整.配合热更新操作。(将xLua原生代码拆分成两部分,编辑器脚本放在主工程,其他部分放在Hotfix工程)
经测试打包,新增方法,Xlua获取新增C#方法均实现。
性能测试:
测试用例:
local function test0()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 0)
local cnt = CS.HybridHotUpdateTool.times * 1000
local go = CS.UnityEngine.GameObject("_")
local transform = go.transform
for i = 1, cnt do
transform.position = transform.position
end
CS.UnityEngine.GameObject.Destroy(go)
CS.HybridHotUpdateTool.EndTime()
end
local function test1()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 1)
local cnt = CS.HybridHotUpdateTool.times * 100
local go = CS.UnityEngine.GameObject("_")
local transform = go.transform
for i = 1, cnt do
transform:Rotate(CS.UnityEngine.Vector3.up, 1)
end
CS.UnityEngine.GameObject.Destroy(go)
CS.HybridHotUpdateTool.EndTime()
end
local function test2()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 2)
local cnt = CS.HybridHotUpdateTool.times * 1000
local go = CS.UnityEngine.GameObject("_")
local transform = go.transform
for i = 1, cnt do
local tmp = CS.UnityEngine.Vector3(i, i, i)
local x = tmp.x
local y = tmp.y
local z = tmp.z
local r = x + y * z
end
CS.HybridHotUpdateTool.EndTime()
end
local function test3()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 3)
local cnt = CS.HybridHotUpdateTool.times * 10
for i = 1, cnt do
local tmp = CS.UnityEngine.GameObject("___")
CS.UnityEngine.GameObject.Destroy(tmp)
end
CS.HybridHotUpdateTool.EndTime()
end
local function test4()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 4)
local cnt = CS.HybridHotUpdateTool.times * 10
for i = 1, cnt do
local tmp = CS.UnityEngine.GameObject("___")
tmp:AddComponent(typeof(CS.UnityEngine.SkinnedMeshRenderer))
local c = tmp:GetComponent(typeof(CS.UnityEngine.SkinnedMeshRenderer))
c.receiveShadows = false
CS.UnityEngine.GameObject.Destroy(tmp)
end
CS.HybridHotUpdateTool.EndTime()
end
local function test5()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 5)
local cnt = CS.HybridHotUpdateTool.times * 1000
for i = 1, cnt do
local tmp = CS.UnityEngine.Input.mousePosition;
end
CS.HybridHotUpdateTool.EndTime()
end
local function test6()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 6)
local cnt = CS.HybridHotUpdateTool.times * 1000
for i = 1, cnt do
local tmp = CS.UnityEngine.Vector3(i, i, i)
CS.UnityEngine.Vector3.Normalize(tmp)
end
CS.HybridHotUpdateTool.EndTime()
end
local function test7()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 7)
local cnt = CS.HybridHotUpdateTool.times * 100
for i = 1, cnt do
local t1 = CS.UnityEngine.Quaternion.Euler(i, i, i)
local t2 = CS.UnityEngine.Quaternion.Euler(i * 2, i * 2, i * 2)
CS.UnityEngine.Quaternion.Slerp(t1, t2, CS.UnityEngine.Random.Range(0.1, 0.9))
end
CS.HybridHotUpdateTool.EndTime()
end
local function test8()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 8)
local cnt = CS.HybridHotUpdateTool.times * 10000
local total = 0
for i = 1, cnt do
total = total + i - (i / 2) * (i + 3) / (i + 5)
end
CS.HybridHotUpdateTool.EndTime()
end
local function test9()
CS.HybridHotUpdateTool.StartTime("lua_test" .. 9)
local cnt = CS.HybridHotUpdateTool.times * 1000
for i = 1, cnt do
local tmp0 = CS.UnityEngine.Vector3(1, 2, 3)
local tmp1 = CS.UnityEngine.Vector3(4, 5, 6)
local tmp2 = tmp0 + tmp1
end
CS.HybridHotUpdateTool.EndTime()
end
private static void Test0()
{
StartTime("CS_Test" + 0);
var go = new GameObject("t");
var transform = go.transform;
var cnt = times * 1000;
for (var i = 0; i < cnt; i++)
{
transform.position = transform.position;
}
UnityEngine.Object.Destroy(go);
EndTime();
}
private static void Test1()
{
StartTime("CS_Test" + 1);
var go = new GameObject("t");
var transform = go.transform;
var cnt = times * 100;
for (var i = 0; i < cnt; i++)
{
transform.Rotate(Vector3.up, 1);
}
UnityEngine.Object.Destroy(go);
EndTime();
}
private static void Test2()
{
StartTime("CS_Test" + 2);
var cnt = times * 1000;
for (var i = 0; i < cnt; i++)
{
var v = new Vector3(i, i, i);
var x = v.x;
var y = v.y;
var z = v.z;
var r = x + y * z;
}
EndTime();
}
private static void Test3()
{
StartTime("CS_Test" + 3);
var cnt = times * 10;
for (var i = 0; i < cnt; i++)
{
var go = new GameObject("t");
UnityEngine.Object.Destroy(go);
}
EndTime();
}
private static void Test4()
{
StartTime("CS_Test" +4);
var cnt = times * 10;
for (var i = 0; i < cnt; i++)
{
var go = new GameObject();
go.AddComponent<SkinnedMeshRenderer>();
var c = go.GetComponent<SkinnedMeshRenderer>();
c.receiveShadows = false;
UnityEngine.Object.Destroy(go);
}
EndTime();
}
private static void Test5()
{
StartTime("CS_Test" + 5);
var cnt = times * 1000;
for (var i = 0; i < cnt; i++)
{
var p = Input.mousePosition;
}
EndTime();
}
private static void Test6()
{
StartTime("CS_Test" + 6);
var cnt = times * 1000;
for (var i = 0; i < cnt; i++)
{
var v = new Vector3(i, i, i);
Vector3.Normalize(v);
}
EndTime();
}
private static void Test7()
{
StartTime("CS_Test" + 7);
var cnt = times * 100;
for (var i = 0; i < cnt; i++)
{
var q1 = Quaternion.Euler(i, i, i);
var q2 = Quaternion.Euler(i * 2, i * 2, i * 2);
Quaternion.Slerp(Quaternion.identity, q1, 0.5f);
}
EndTime();
}
private static void Test8()
{
StartTime("CS_Test" + 8);
long total = 0;
var cnt = times * 10000;
for (var i = 0; i < cnt; i++)
{
total = total + i - (i / 2) * (i + 3) / (i + 5);
}
EndTime();
}
private static void Test9()
{
StartTime("CS_Test" + 9);
var cnt = times * 1000;
for (var i = 0; i < cnt; i++)
{
var a = new Vector3(1, 2, 3);
var b = new Vector3(4, 5, 6);
var c = a + b;
}
EndTime();
}
测试结果:
Hybrid - IOS测试:
libil2cpp.a 编译
确保你的macOS版本>=12以及xcode版本>=13
点击菜单 生成所有必要的文件
HybridCLRData/Generate/All
安装Camke
brew install cmake
坑点(需要优化,处理):
- Did you #ifdef UNITY_EDITOR a section of your serialized properties in any of your scripts?
热更dll中,不能用 UNITY_EDITOR 宏, compile之后,已经没有所谓的宏了。
- dll裁剪生成导出是在打包时,所以需要二次打包(要整理出包流程,当前是用二次出包,需要优化)
- 暂不支持:
- GetComponent(string name)方式无法获得组件(PC,和真机不一致,打包后报错风险)
- IOS打包需要生成并替换Xcode工程中的libil2cpp.a文件。
- 某些Unity版本需要手动修改Unity编辑器相关dll.(目前2020.3.29不需要)
- Unity 安装目录必须包含版本号,否则无法识别版本。