• 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;
    }
    View Code

    然后就是注册委托的适配器和转换器了,这个就自己看需求来了

    加载热更文件很简单,接下来要说的就是如何简单的去执行和注册热更代码

    对于执行热更代码,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项目中有,大家可以通过下方的地址下载

  • 相关阅读:
    JAVA线程池原理详解一
    并发工具类:CountDownLatch、CyclicBarrier、Semaphore
    JAVA并行框架:Fork/Join
    Mock Server实践
    MockWebServer使用指南
    mybatis学习笔记 spring与mybatis整合
    怎样使用Mock Server
    基于unittest测试框架的扩展
    运营商劫持
    单元测试实战手册
  • 原文地址:https://www.cnblogs.com/unityExplorer/p/13540784.html
Copyright © 2020-2023  润新知