版权声明:本文为原创文章,转载请声明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; }