unity探索者之ILRuntime代码热更新
版权声明:本文为原创文章,转载请声明https://www.cnblogs.com/unityExplorer/p/13540784.html
最近几年,随着游戏研发质量越来越高,游戏包体大小也是增大不少,热更新功能就越发显的重要。
两、三年前曾用过xlua作为热更方式,xlua的热补丁方式对于bug修复、修改类和函数之类的热更还是比较好用的
但是lua对于中小型团队并不是那么友好,毕竟会lua的人始终只有一部分,更多的unity开发者还是对c#更熟悉一些
原本c#是有动态编译功能的,也就是支持热更新,奈何ios系统不支持jit,禁止mono的动态编译,并且虽然android支持动态编译,但实际使用dll热更的时候坑也不少
于是在ILRuntime的正式版1.0出来后,立马就去体验了一下,果然用起来还不错
截止到目前,ILRuntime的版本已经更新到1.6.4,从1.6开始,ILRuntime也发布到了unity的Package Manager,集成也比之前更方便
如果你使用的是unity2018或更高的版本,那可以直接在Package Manager中找到ILRuntime的包,或者按照ILRuntime的官网说明来集成
如果你使用的是unity2017或更低的版本,官网里也有官方SDK的下载地址
这是ILRuntime的官网:https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html
因为ILRuntime使用unsafe代码,所以在导入SDK后还需要在设置中允许unsafe代码,位置在Player Settings -> Other Setttings
说了这么多,该说点干货了,我先说说怎么使用和加载热更新文件吧
很多博客中在讲ILRuntime热更新文件的加载时候,都是直接使用WWW下载/加载热更dll文件,包括ILRuntime的官网中给的示例也是这样
然而在unity2017乃至更高的版本中,WWW已经被UnityWebRequest取代,并且WWW异步加载本地文件的速度是很慢的,当然这是小问题
重点是dll文件,dll文件的问题在于安全性并不高,有太多的的反编译工具可以将dll文件反编译出来
虽然你可以对dll进行加密或者混淆,但是这又会带来更多新的问题
所以最终我选择将热更项目生成的dll文件打成bundle,然后通过AssetBundle.LoadAsset<TextAsset>()读取。
public static AppDomain appdomain;
static AssetBundle hotfixAB;
/// <summary> /// 加载热更补丁 /// </summary>
public static void LoadHotFix()
{
if (hotfixAB)
hotfixAB.Unload(true);
hotfixAB = AssetBundle.LoadFromFile("你的热更bundle文件地址");
if (hotfixAB)
{
appdomain = new AppDomain();
//加载热更主体,也就是dll文件
TextAsset taHotFix = hotfixAB.LoadAsset<TextAsset>("hotfix");
if (!taHotFix) return;
using (MemoryStream ms = new MemoryStream(taHotFix.bytes))
{
//加载pdb文件,测试用,正式版只需要加载热更主体
TextAsset taHotFixPdb = hotfixAB.LoadAsset<TextAsset>("hotfixpdb");
if (!taHotFixPdb)
return;
using (MemoryStream msp = new MemoryStream(taHotFixPdb.bytes))
{
//加载热更的核心函数,如果是正式版,则只传主体就可以:appdomain.LoadAssembly(ms);
appdomain.LoadAssembly(ms, msp, new PdbReaderProvider());
}
}
}
}
这种方式实际上是以字节流的形式加载热更代码,而bundle实际上也可以通过LoadFromMemory以字节流的形式加载bundle文件,这就意味着你可以任意使用各种加密方式来保证热更代码的安全性(当然资源也可以使用这种方式来进行加密)
如何加密bundle这里就不多说了,很多博主都讲过,大家可以自行搜索
因为unity组件的特殊性,加载完热更代码后,还需要解决跨域继承和Component的重定向问题
这两个问题在ILRuntime的官网都有说明,这里就不多说,直接上代码了
static void InitializeILRuntime() { SetupCLRRedirectionAddComponent();//设置AddComponent的重定向 SetupCLRRedirectionGetComponent();//设置GetComponent的重定向 appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());//绑定Coroutine适配器 appdomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());//绑定MonoBehaviour适配器 JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);//注册LitJson的重定向 } unsafe static void SetupCLRRedirectionAddComponent() { var arr = typeof(GameObject).GetMethods(); foreach (var i in arr) { if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1) { appdomain.RegisterCLRMethodRedirection(i, AddComponent); } } } unsafe static void SetupCLRRedirectionGetComponent() { var arr = typeof(GameObject).GetMethods(); foreach (var i in arr) { if (i.Name == "GetComponent" && i.GetGenericArguments().Length == 1) { appdomain.RegisterCLRMethodRedirection(i, GetComponent); } } } unsafe static StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj) { //CLR重定向的说明请看相关文档和教程,这里不多做解释 AppDomain __domain = __intp.AppDomain; var ptr = __esp - 1; //成员方法的第一个参数为this GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject; if (instance == null) throw new NullReferenceException(); __intp.Free(ptr); var genericArgument = __method.GenericArguments; //AddComponent应该有且只有1个泛型参数 if (genericArgument != null && genericArgument.Length == 1) { var type = genericArgument[0]; object res; if (type is CLRType) { //Unity主工程的类不需要任何特殊处理,直接调用Unity接口 res = instance.AddComponent(type.TypeForCLR); } else { //热更DLL内的类型比较麻烦。首先我们得自己手动创建实例 var ilInstance = new ILTypeInstance(type as ILType, false);//手动创建实例是因为默认方式会new MonoBehaviour,这在Unity里不允许 //接下来创建Adapter实例 var clrInstance = instance.AddComponent<MonoBehaviourAdapter.Adaptor>(); //unity创建的实例并没有热更DLL里面的实例,所以需要手动赋值 clrInstance.ILInstance = ilInstance; clrInstance.AppDomain = __domain; //这个实例默认创建的CLRInstance不是通过AddComponent出来的有效实例,所以得手动替换 ilInstance.CLRInstance = clrInstance; res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance clrInstance.Awake();//因为Unity调用这个方法时还没准备好所以这里补调一次 clrInstance.OnEnable();//因为Unity调用这个方法时还没准备好所以这里补调一次 } return ILIntepreter.PushObject(ptr, __mStack, res); } return __esp; } unsafe static StackObject* GetComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj) { //CLR重定向的说明请看相关文档和教程,这里不多做解释 AppDomain __domain = __intp.AppDomain; var ptr = __esp - 1; //成员方法的第一个参数为this GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject; if (instance == null) throw new NullReferenceException(); __intp.Free(ptr); var genericArgument = __method.GenericArguments; //GetComponent应该有且只有1个泛型参数 if (genericArgument != null && genericArgument.Length == 1) { var type = genericArgument[0]; object res = null; if (type is CLRType) { //Unity主工程的类不需要任何特殊处理,直接调用Unity接口 res = instance.GetComponent(type.TypeForCLR); } else { //因为所有DLL里面的MonoBehaviour实际都是这个Component,所以我们只能全取出来遍历查找 var clrInstances = instance.GetComponents<MonoBehaviourAdapter.Adaptor>(); for (int i = 0; i < clrInstances.Length; i++) { var clrInstance = clrInstances[i]; if (clrInstance.ILInstance != null)//ILInstance为null, 表示是无效的MonoBehaviour,要略过 { if (clrInstance.ILInstance.Type == type) { res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance break; } } } } return ILIntepreter.PushObject(ptr, __mStack, res); } return __esp; }
然后就是注册委托的适配器和转换器了,这个就自己看需求来了
加载热更文件很简单,接下来要说的就是如何简单的去执行和注册热更代码
对于执行热更代码,ILRuntime封装出来的的用发很简单
调用热更代码的核心函数就四行
if (appdomain.LoadedTypes[typeFullName] is ILType type) { IMethod im = type.GetMethod(methodName); if (im != null) appdomain.Invoke(im, instance, p); }
当然,实际开发中肯定不止这几行代码,对于不同情况,我们可能需要做出不同的处理方案
此外,在实际开发中,也许大部分的函数都需要增加这些代码,所以,最好的办法就是将热更的检测和执行代码封装到一个函数中
//因为程序运行过程中,函数可能会被执行很多次,为了效率,我们将所有被检测过的函数都保存在字典中 private Dictionary<string, IMethod> iMethods = new Dictionary<string, IMethod>();
//returnObject:热更函数执行成功后的返回值,若无返回值或热更函数不存在,则为null protected bool TryInvokeHotFix(out object returnObject, params object[] p) { returnObject = null;
//对于非静态函数,需要先创建到热更类的对象 object instanceHotFix = appdomain.Instantiate(typeName); if (instanceHotFix != null) {
//通过c#反射提供的接口获取到执行热更检测的函数信息 MethodBase method = new StackFrame(1).GetMethod(); string methodName = method.Name; int paramCount = method.GetParameters().Length;
//这里将函数名和参数数量进行拼接来作为存储的key
//当然,如果你确实存在函数名和参数数量均相同,但是参数类型不同的函数的热更需求,你也可以从GetParameters()中获取到所有参数的类型,自定义key的组合方式 string key = methodName + "_" + paramCount.ToString(); IMethod im; if (iMethods.ContainsKey(key)) im = iMethods[key]; else { im = type.GetMethod(methodName, paramCount); iMethods.Add(key, im); } if (im != null) { returnObject = appdomain.Invoke(im, instanceHotFix, p); return true; } } return false; }
上面是非静态函数的热更检测执行方法,用起来也很简单,只要在函数内的头部执行以下代码就OK
public int Test(int test) { if (TryInvokeHotFix(out object ob, test)) return (int)ob; return test; }
对于没有参数的函数,然后将参数部分传null,避免new object[],减少GC
对于没有返回值的,去掉返回值部分就好
if (TryInvokeHotFix(out object ob, null)) return;
上面是非静态函数的热更方法,对于静态函数,结构大体相同,但是函数内部稍微有点区别
protected static bool TryInvokeStaticHotFix(out object returnObject, params object[] p) { returnObject = null;if (!appdomain.LoadedTypes.ContainsKey(typeFullName)) return false; if (appdomain.LoadedTypes[typeFullName] is ILType type) {
MethodBase method = new StackFrame(1).GetMethod(); IMethod im = type.GetMethod(method.Name, method.GetParameters().Length); if (im != null) { returnObject = appdomain.Invoke(im, null, p); return true; } } return false; }
调用方法就不写了,和非静态一样
除了这两个核心函数外,还有关于初始化及一些容错处理,这里我就不写了,完整的代码和测试样例在我的Git项目中有,大家可以通过下方的地址下载