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项目中有,大家可以通过下方的地址下载

posted @ 2020-09-10 14:13  半颗星辰  阅读(1501)  评论(1编辑  收藏  举报