ILRuntime热更新 Unity的C#热更新方案(2018)


1 热更新方案总结

https://gameinstitute.qq.com/community/detail/119660

2 ILRuntime热更新Demo

https://github.com/Ourpalm/ILRuntimeU3D

3 ILRuntime官网

http://ourpalm.github.io/ILRuntime/public/v1/guide/index.html

4 ILRuntime和xlua等方案比较

 http://dy.163.com/v2/article/detail/DD9LQ1AJ0511L9VL.html

5 ILRuntime Git

https://github.com/Ourpalm/ILRuntime

 

Git上面的Demo是用2019实现的。2018以下需要自己手动拉取git源码。再按照说明装入整合。

 

在Unity中使用Github的master分支

如果你希望在Unity中使用ILRuntime的最新master版本
你需要将下列源码目录复制Unity工程的Assets目录:

  • Dependencies
  • ILRuntime

需要注意的是,需要删除这些目录里面的binobjProperties子目录,以及.csproj文件。此外,由于ILRuntime使用了unsafe代码来优化执行效率,所以你需要在Unity中开启unsafe模式:

  • Unity2017以上的版本请在PlayerSettings中勾选Allow unsafe mode
  • Assets目录里建立一个名为smcs.rsp的文本文件
  • smcs.rsp文件中加入 -unsafe
  • 如果你使用的是Unity5.4及以前的版本,并且使用的编译设置是.Net 2.0而不是.Net 2.0 Subset的话,你需要将上述说明中的smcs.rsp文件名改成gmcs.rsp
  • 如果你使用的是Unity5.5以上的版本,你需要将上述说明中的smcs.rsp文件名改成mcs.rsp

从Visual Studio开始

如果你希望在VisualStudio的C#项目中使用ILRuntime, 你只需要引用编译好的ILRuntime.dllILRuntim.Mono.Cecil.dll以及ILRuntime.Mono.Cecil.Pdb.dll即可。

 

整理完毕的项目目录如下

 

 

 

Dependencies , ILRuntime,LitJson 直接使用从源码中copy进来。

Example中的cs文件对应ILRuntimeDemo中的各种示例用法。其中都有很详细的中文注释。

待Unity项目成功编译通过生成 Assembly-CSharp.dll

StreamingAssets  中的HotFix_project.dll HotFix_project.pdb 是由另一个HotFix_project项目生成的我们用来热更新的代码。 

这里就是我们ILRuntime使用的外壳环境了。

 

热更新的关键代码就在这里。

//PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif

外壳启动后动态去加载HotFix_project.。从示例中的各种方法调用其内部。

  void OnHotFixLoaded()
    {
        //HelloWorld,第一次方法调用
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunHelloWorld", null, null);
    }

 

Hotfix_project 项目设置

 

 这里需要引用相关的dll。

UnityEngine就在Hotfix目录下的/UnityDlls/

Assembly=CSharp是我们外壳环境编译生成的dll

 

最后将这个项目的编译生成位置指定到上面代码加载的 /streamingAssetsPath/

前后贯通 即可使用了。

 

详细的调用方法

1 静态方法 ,泛型方法

Debug.Log("调用无参数静态方法");
        //调用无参数静态方法,appdomain.Invoke("类名", "方法名", 对象引用, 参数列表);
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
        //调用带参数的静态方法
        Debug.Log("调用带参数的静态方法");
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);


        Debug.Log("通过IMethod调用方法");
        //预先获得IMethod,可以减低每次调用查找方法耗用的时间
        IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        //根据方法名称和参数个数获取方法
        IMethod method = type.GetMethod("StaticFunTest2", 1);

        appdomain.Invoke(method, null, 123);

        Debug.Log("通过无GC Alloc方式调用方法");
        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushInteger(123);
            ctx.Invoke();
        }

        Debug.Log("指定参数类型来获得IMethod");
        IType intType = appdomain.GetType(typeof(int));
        //参数类型列表
        List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
        paramList.Add(intType);
        //根据方法名称和参数类型列表获取方法
        method = type.GetMethod("StaticFunTest2", paramList, null);
        appdomain.Invoke(method, null, 456);

        Debug.Log("实例化热更里的类");
        object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });
        //第二种方式
        object obj2 = ((ILType)type).Instantiate();

        Debug.Log("调用成员方法");
        method = type.GetMethod("get_ID", 0);
        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushObject(obj);
            ctx.Invoke();
            int id = ctx.ReadInteger();
            Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
        }

        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushObject(obj2);
            ctx.Invoke();
            int id = ctx.ReadInteger();
            Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
        }
        
        Debug.Log("调用泛型方法");
        IType stringType = appdomain.GetType(typeof(string));
        IType[] genericArguments = new IType[] { stringType };
        appdomain.InvokeGenericMethod("HotFix_Project.InstanceClass", "GenericMethod", genericArguments, null, "TestString");

        Debug.Log("获取泛型方法的IMethod");
        paramList.Clear();
        paramList.Add(intType);
        genericArguments = new IType[] { intType };
        method = type.GetMethod("GenericMethod", paramList, genericArguments);
        appdomain.Invoke(method, null, 33333);

        Debug.Log("调用带Ref/Out参数的方法");
        method = type.GetMethod("RefOutMethod", 3);
        int initialVal = 500;
        using(var ctx = appdomain.BeginInvoke(method))
        {
            //第一个ref/out参数初始值
            ctx.PushObject(null);
            //第二个ref/out参数初始值
            ctx.PushInteger(initialVal);
            //压入this
            ctx.PushObject(obj);
            //压入参数1:addition
            ctx.PushInteger(100);
            //压入参数2: lst,由于是ref/out,需要压引用,这里是引用0号位,也就是第一个PushObject的位置
            ctx.PushReference(0);
            //压入参数3,val,同ref/out
            ctx.PushReference(1);
            ctx.Invoke();
            //读取0号位的值
            List<int> lst = ctx.ReadObject<List<int>>(0);
            initialVal = ctx.ReadInteger(1);

            Debug.Log(string.Format("lst[0]={0}, initialVal={1}", lst[0], initialVal));
        }

2  委托

委托适配器(DelegateAdapter)

如果将委托实例传出给ILRuntime外部使用,那就意味着需要将委托实例转换成真正的CLR(C#运行时)委托实例,这个过程需要动态创建CLR的委托实例。由于IL2CPP之类的AOT编译技术无法在运行时生成新的类型,所以在创建委托实例的时候ILRuntime选择了显式注册的方式,以保证问题不被隐藏到上线后才发现。

 

委托转换器(DelegateConvertor)

ILRuntime内部是使用Action,以及Func这两个系统自带委托类型来生成的委托实例,所以如果你需要将一个不是Action或者Func类型的委托实例传到ILRuntime外部使用的话,除了委托适配器,还需要额外写一个转换器,将Action和Func转换成你真正需要的那个委托类型。

 

//TestDelegateMethod, 这个委托类型为有个参数为int的方法,注册仅需要注册不同的参数搭配即可
appdomain.DelegateManager.RegisterMethodDelegate<int>();
//带返回值的委托的话需要用RegisterFunctionDelegate,返回类型为最后一个
appdomain.DelegateManager.RegisterFunctionDelegate<int, string>();
//Action<string> 的参数为一个string
appdomain.DelegateManager.RegisterMethodDelegate<string>();

//ILRuntime内部是用Action和Func这两个系统内置的委托类型来创建实例的,所以其他的委托类型都需要写转换器
//将Action或者Func转换成目标委托类型

appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateMethod>((action) =>
{
//转换器的目的是把Action或者Func转换成正确的类型,这里则是把Action<int>转换成TestDelegateMethod
return new TestDelegateMethod((a) =>
{
//调用委托实例
((System.Action<int>)action)(a);
});
});
//对于TestDelegateFunction同理,只是是将Func<int, string>转换成TestDelegateFunction
appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateFunction>((action) =>
{
return new TestDelegateFunction((a) =>
{
return ((System.Func<int, string>)action)(a);
});
});

//下面再举一个这个Demo中没有用到,但是UGUI经常遇到的一个委托,例如UnityAction<float>
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction<float>>((action) =>
{
return new UnityEngine.Events.UnityAction<float>((a) =>
{
((System.Action<float>)action)(a);
});
});

 

void OnHotFixLoaded()
{
Debug.Log("完全在热更DLL内部使用的委托,直接可用,不需要做任何处理");

Debug.Log("如果需要跨域调用委托(将热更DLL里面的委托实例传到Unity主工程用), 就需要注册适配器");
Debug.Log("这是因为iOS的IL2CPP模式下,不能动态生成类型,为了避免出现不可预知的问题,我们没有通过反射的方式创建委托实例,因此需要手动进行一些注册");
Debug.Log("如果没有注册委托适配器,运行时会报错并提示需要的注册代码,直接复制粘贴到ILRuntime初始化的地方");
appdomain.Invoke("HotFix_Project.TestDelegate", "Initialize2", null, null);
appdomain.Invoke("HotFix_Project.TestDelegate", "RunTest2", null, null);
Debug.Log("运行成功,我们可以看见,用Action或者Func当作委托类型的话,可以避免写转换器,所以项目中在不必要的情况下尽量只用Action和Func");
Debug.Log("另外应该尽量减少不必要的跨域委托调用,如果委托只在热更DLL中用,是不需要进行任何注册的");
Debug.Log("---------");
Debug.Log("我们再来在Unity主工程中调用一下刚刚的委托试试");
TestMethodDelegate(789);
var str = TestFunctionDelegate(098);
Debug.Log("!! OnHotFixLoaded str = " + str);
TestActionDelegate("Hello From Unity Main Project");

}

 

 

 

4 使用热更新中的接口 创建热更中的类实例

Unity中 

public abstract class TestClassBase
{
    public virtual int Value
    {
        get
        {
            return 0;
        }
        set
        {

        }
    }

    public virtual void TestVirtual(string str)
    {
        Debug.Log("!! TestClassBase.TestVirtual, str = " + str);
    }

    public abstract void TestAbstract(int gg);
}

Hotfix

   public class TestInheritance : TestClassBase
    {
        public override int Value { get; set; }
        public override void TestAbstract(int gg)
        {
            UnityEngine.Debug.Log("!! TestInheritance.TestAbstract gg =" + gg);
        }

        public override void TestVirtual(string str)
        {
            base.TestVirtual(str);
            UnityEngine.Debug.Log("!! TestInheritance.TestVirtual str =" + str);
        }

        public static TestInheritance NewObject()
        {
            return new HotFix_Project.TestInheritance();
        }
    }

实际Unity工程使用时要通过适配器注册来创建实例

  Debug.Log("首先我们来创建热更里的类实例");
        TestClassBase obj;
        Debug.Log("现在我们来注册适配器, 该适配器由ILRuntime/Generate Cross Binding Adapter菜单命令自动生成");
        appdomain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
        Debug.Log("现在再来尝试创建一个实例");
        obj = appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");
        Debug.Log("现在来调用成员方法");
        obj.TestAbstract(123);
        obj.TestVirtual("Hello");
        obj.Value = 233;
        Debug.LogFormat("obj.Value={0}", obj.Value);


        Debug.Log("现在换个方式创建实例");
        obj = appdomain.Invoke("HotFix_Project.TestInheritance", "NewObject", null, null) as TestClassBase;
        obj.TestAbstract(456);
        obj.TestVirtual("Foobar");
        obj.Value = 2333333;
        Debug.LogFormat("obj.Value={0}", obj.Value);

5 CLR 重定向

...略

6 CLR绑定

通常情况下,如果要从热更DLL中调用Unity主工程或者Unity的接口,是需要通过反射接口来调用的,包括市面上不少其他热更方案,也是通过这种方式来对CLR方接口进行调用的。

但是这种方式有着明显的弊端,最突出的一点就是通过反射来调用接口调用效率会比直接调用低很多,再加上反射传递函数参数时需要使用object[]数组,这样不可避免的每次调用都会产生不少GC Alloc。众所周知GC Alloc高意味着在Unity中执行会存在较大的性能问题。

ILRuntime通过CLR方法绑定机制,可以选择性的对经常使用的CLR接口进行直接调用,从而尽可能的消除反射调用开销以及额外的GC Alloc

 

7 协程调用 同样需要注册适配器

 //这里做一些ILRuntime的注册
        //使用Couroutine时,C#编译器会自动生成一个实现了IEnumerator,IEnumerator<object>,IDisposable接口的类,因为这是跨域继承,所以需要写CrossBindAdapter(详细请看04_Inheritance教程),Demo已经直接写好,直接注册即可
        appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
unsafe void OnHotFixLoaded()
{
  appdomain.Invoke("HotFix_Project.TestCoroutine", "RunTest", null, null);
}

public void DoCoroutine(IEnumerator coroutine)
{
  StartCoroutine(coroutine);
}

 

8 在热更中使用 MonoBehaviour

。。。略

 

9 Unity工程中反射热更中的类和属性

Debug.Log("C#工程中反射是一个非常经常用到功能,ILRuntime也对反射进行了支持,在热更DLL中使用反射跟原生C#没有任何区别,故不做介绍");
        Debug.Log("这个Demo主要是介绍如何在主工程中反射热更DLL中的类型");
        Debug.Log("假设我们要通过反射创建HotFix_Project.InstanceClass的实例");
        Debug.Log("显然我们通过Activator或者Type.GetType(\"HotFix_Project.InstanceClass\")是无法取到类型信息的");
        Debug.Log("热更DLL中的类型我们均需要通过AppDomain取得");
        var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        Debug.Log("LoadedTypes返回的是IType类型,但是我们需要获得对应的System.Type才能继续使用反射接口");
        var type = it.ReflectionType;
        Debug.Log("取得Type之后就可以按照我们熟悉的方式来反射调用了");
        var ctor = type.GetConstructor(new System.Type[0]);
        var obj = ctor.Invoke(null);
        Debug.Log("打印一下结果");
        Debug.Log(obj);
        Debug.Log("我们试一下用反射给字段赋值");
        var fi = type.GetField("id", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        fi.SetValue(obj, 111111);
        Debug.Log("我们用反射调用属性检查刚刚的赋值");
        var pi = type.GetProperty("ID");
        Debug.Log("ID = " + pi.GetValue(obj, null));

 

10  Unity值类型绑定

注册值类型绑定 用来提高效率

 //这里做一些ILRuntime的注册,这里我们注册值类型Binder,注释和解注下面的代码来对比性能差别
        appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
        appdomain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
        appdomain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());

 

Debug.Log("Vector3等Unity常用值类型如果不做任何处理,在ILRuntime中使用会产生较多额外的CPU开销和GC Alloc");
Debug.Log("我们通过值类型绑定可以解决这个问题,只有Unity主工程的值类型才需要此处理,热更DLL内定义的值类型不需要任何处理");
Debug.Log("请注释或者解注InitializeILRuntime里的代码来对比进行值类型绑定前后的性能差别");

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM