zoukankan      html  css  js  c++  java
  • 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项目中有,大家可以通过下方的地址下载

  • 相关阅读:
    107. Binary Tree Level Order Traversal II
    108. Convert Sorted Array to Binary Search Tree
    111. Minimum Depth of Binary Tree
    49. Group Anagrams
    使用MALTAB标定实践记录
    442. Find All Duplicates in an Array
    522. Longest Uncommon Subsequence II
    354. Russian Doll Envelopes
    opencv 小任务3 灰度直方图
    opencv 小任务2 灰度
  • 原文地址:https://www.cnblogs.com/unityExplorer/p/13540784.html
Copyright © 2011-2022 走看看