Unity 接入 ILRuntime 热更方案

引言

最近看了一下 ET 框架,本来只是研究一下网络模块,后来抽时间看一下热更框架。ET 的热更使用的不是像 tolua 这样基于 Lua 的方案,而是基于 ILRuntime 的纯 C# 热更实现方案。

ILRuntime 的实现原理

对 Unity 引擎有一定了解的开发者都应该知道: Unity 支持使用 C# 做脚本语言,是依赖 Mono 引擎运行 C# 编译后的 IL 中间语言。ILRuntime 借助 Mono.Cecil 库来读取 DLL 的 PE 信息,以及当中类型的所有信息,最终得到方法的 IL 汇编码,然后通过内置的 IL 解译执行虚拟机来执行 DLL 中的代码。

Cecil 是一个用来生成(修改和创建)和检查 ECMA CIL 格式的程序和库的库,可以完成如下操作:

使用简单而强大的对象模型分析 .NET 二进制文件,无需通过加载程序集即可使用 Reflection(反射)

修改 .NET 二进制文件,添加新的元数据结构并更改 IL 代码

Cecil 官网: http://cecil.pe

相关资源

ILRuntime 是 2016 年发布的一个开源项目,2017 年发布了第一个正式版,地址 Ourpalm/ILRuntime

$ git clone https://github.com/Ourpalm/ILRuntime.git

官方提供的 Unity Demo

官方中文文档 ILRuntime Doc 其中包含:

教程
从零开始
ILRuntime中使用委托
ILRuntime中跨域继承
ILRuntime中的反射
CLR重定向
CLR绑定
LitJson集成

其他
IL2CPP打包注意事项
ILRuntime的性能优化
ILRuntime的实现原理

Unity 集成步骤

参考官方文档 从零开始,基本就一下几个步骤:

  • 下载最新的 release 版本 ILRuntime-1.4.zip ,然后解压缩
  • 将 ILRuntime 源码工程下的 Mono.Cecil.20 、Mono.Cecil.Pdb 和 ILRuntime 复制到 Unity 工程 Assets 目录下
需删除这些目录下的 bin 、obj 和 Properties 子目录,还有 .csproj 文件
  • Unity 开启 unsafe 模式
    • 在 Assets 目录下创建一个命名为 smcs.rsp 文本文件,内容为 -unsafe
Unity5.4 及以前的版本,且编译设置是 .Net 2.0 而不是 .Net 2.0 Subset 的话, 需要将 smcs.rsp 文件名改成 gmcs.rsp
Unity5.5 以上的版本,需要将 smcs.rsp 文件名改成 mcs.rsp
  • 创建热更工程(Hotfix)

热更工程是一个独立于 Unity 工程的一个独立的 C# 类库工程,这里需用借助 VS 2017 来完成创建操作,创建步骤如下:

  • 使用 VS 2017 打开当前 Unity 工程的 .sln 文件
  • 文件 - 新建 - 项目 ,选择 Visual C# 栏 中的 类库(.NET Framework) ,然后完成剩余的设置:
1.命名为 Hotfix 
2.位置是 Unity 工程根目录
3.解决方案为 添加到解决方案 (即添加到当前 Unity 的解决方案内)
4.框架设置为与 Unity 工程版本一样的 .NET Framwork 4.7.1(根据自己项目情况选择)
image.png

然后点击 确定 创建出 Hotfix 工程。

  • 在 解决方案管理器 中选择 Hotfix 工程的 引用 选项,右键 - 添加引用 ,通过 浏览 按钮依次添加如下四个库文件:

Unity 引擎自带的工具库

Unity引擎安装目录\Editor\Data\Managed\UnityEngine\UnityEngine.dll
Unity引擎安装目录\Editor\Data\Managed\UnityEngine\UnityEngine.CoreModule.dll
Unity引擎安装目录\Editor\Data\UnityExtensions\Unity\GUISystem\UnityEngine.UI.dll

UnityEngine.CoreModule.dll 是在 Unity2017.2 之后的版本才有,低版本的 Unity 无需添加此文件
Unity 工程的业务代码库

Unity工程目录\Library\ScriptAssemblies\Assembly-CSharp.dll
image.png

到这里 Hotfix 工程就创建成功了,下面便是具体的测试代码。

测试代码

这个要测试两个方面,一是在 Hotfix 工程中调用 Unity 的接口,二是在 Unity 工程中调用 Hotfix 提供的接口。这里参考 ILRuntime中的反射 文档即可实现,具体如下:

  • 创建场景

在 Unity 工程中创建一个空场景,添加一个 UI 相机、Canvas 和 一个测试按钮,结构如下:


image.png

从 Unity 中调用 Hotfix 提供的静态方法和非静态方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace Hotfix
{
    public class Test
    {
        // 不带参
        public static String GetMsg()
        {
            Debug.Log("call static GetMsg");
            return "Test Hotfix, static";
        }
        // 带参
        public static String GetMsg1(int num)
        {
            Debug.Log("call static GetMsg1, num = " + num);
            return "Test Hotfix, static, num = " + num;
        }
        // 非静态
        public String GetMsg2(){
            Debug.Log("call GetMsg2");
            return "Test Hotfix, no static";
        }
    }
}

Unity 工程中调用的逻辑如下:

void ILRuntimeTest(){
    Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg", null, null));
    object[] param = new object[1];
    param[0] = 666;
    Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg1", null, param));
    // 创建一个 Test 对象
    var testInst = appdomain.Instantiate("Hotfix.Test");
    Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg2", testInst, null));
}

这里 appdomain 是一个 ILRuntime.Runtime.Enviorment.AppDomain 实例对象,需要加载 Hotfix.dll 后才能开始执行上述的测试方法。

  • 从 Unity 提供给 Hotfix 调用的静态和非静态方法:
    • 调用 Unity 工程中的类方法
      上面的测试代码其实用到了 Unity 的 Engine.Debug.Log 接口,基本是直接调用 Unity 工程中的类方法,但
      这样存在性能问题,后面通过 CLR 绑定来优化性能。

    • 继承 Unity 工程中的类或实现接口
      假如需要在 Hotfix 工程中继承 Unity 工程中的类或实现接口,则需要在 Unity 工程中增加对应类或接口的
      适配器。

    • 使用 Unity 中的值类型,如:Vector3、Vector2 等
      也是可以直接调用,但也需要使用 CLR 绑定来做性能优化。

    • Hotfix 工程中使用委托
      假如只是 Hotfix 工程内部使用的委托,无需做任何额外操作(因为委托是 C# 的特性,而非 Unity 的)。
      但假如需要将 Hotfix 工程中的委托实例传给 Unity 工程,也需要根据情况添加额外的适配器和转化器。

这里在 Unity 工程中定义一个 IUIBase 的接口:

public interface IUIBase{
    void Show();
    void Hide();
    string GetStr();
}

对应的 Unity 工程中得定义一个 IUIBaseAdapter 适配器:

using System;
using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;

public class IUIBaseAdapter : CrossBindingAdaptor
{
    public override Type BaseCLRType{
        get { return typeof (IUIBase); }
    }
    public override Type AdaptorType{
        get { return typeof (Adaptor); }
    }
    public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance){
        return new Adaptor(appdomain, instance);
    }

    public class Adaptor : IUIBase, CrossBindingAdaptorType
    {
        ILTypeInstance instance;
        ILRuntime.Runtime.Enviorment.AppDomain appdomain;

        IMethod mHide;
        IMethod mGetStr;
        IMethod mShow;

        public Adaptor(){}

        public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
        {
            this.appdomain = appdomain;
            this.instance = instance;
        }
        public ILTypeInstance ILInstance {get { return instance; } }

        public void Hide()
        {
            if(mHide == null){
                mHide = instance.Type.GetMethod("Hide", 0);
            }
            if(mHide != null){
                this.appdomain.Invoke(this.mHide, instance, null);
            }
        }

        public void Show()
        {
            if(mShow == null){
                mShow = instance.Type.GetMethod("Show", 0);
            }
            if(mShow != null){
                this.appdomain.Invoke(this.mShow, instance, null);
            }
        }

        public string GetStr(){
            if(mGetStr == null){
                mGetStr = instance.Type.GetMethod("GetStr", 0);
            }
            if(mGetStr != null){
                return this.appdomain.Invoke(this.mGetStr, instance, null).ToString();
            }
            return "";
        }
    }
}

在 Hotfix 工程中让 Test 类实现此接口:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace Hotfix
{
    public class Test : IUIInterface
    {
        ...
        public void Hide()
        {
            Debug.Log("TestUI Hide");
        }

        public void Show()
        {
            Debug.Log("TestUI Show");
            Button btn = GameObject.Find("Canvas/Button").GetComponent<Button>();
            btn.onClick.AddListener(OnClick);
        }

        public string GetStr(){
            return "Test GetStr";
        }
        
        void OnClick(){
            Debug.Log("OnClick Btn");
        }
    }
}

在 Unity 工程中初始化 ILRuntime 时绑定适配器:

appdomain.RegisterCrossBindingAdaptor(new IUIInterfaceAdapter());

创建 Test 对象并调用接口方法:

var testInst = appdomain.Instantiate<IUIInterface>("Hotfix.Test");
testInst.Hide();
Debug.Log(testInst.GetStr());

委托

这里主要分析将 Hotfix 工程内的委托实例传给外部使用的情况,此时需要将委托实例转换成真正的 CLR(C#运行时)委托实例,即通过动态创建 CLR 的委托实例。由于 IL2CPP 之类的 AOT 编译技术无法在运行时生成新的类型,所以在创建委托实例的时候 ILRuntime 选择了显示注册的方式,以保证问题不被隐藏到线上才发现。

  • 委托适配器:
    参数组合一致的各种 delegate 与 Action/Func 可以共用同一个委托适配器:(Func 是有返回值的泛型委
    托)
delegate void SomeDelegate(int a, float b);
Action<int, float> act;

适配器无需单独定义脚本,只需在 Unity 工程初始化 ILRuntime 的 AppDomain 时注册即可,如:

appDomain.DelegateManager.RegisterMethodDelegate<int, float>();

带返回值类型的委托:(Action 是无返回值的泛型委托)

delegate bool SomeFunction(int a, float b);
Func<int, float, bool> act;

注册如下:

appDomain.DelegateManager.RegisterFunctionDelegate<int, float, bool>();
  • 委托转换器:
    ILRuntime 内是使用 Action 和 Func 两个系统自带的委托类型来生成委托实例的,因此如果在 Hotfix 工程中用到的非 Action 和 Func 格式定义的委托实例要传给 Unity 工程中使用,需要在注册委托的地方通过转换器转成真正需要的委托类型:
app.DelegateManager.RegisterDelegateConvertor<SomeFunction>((action) =>
{
    return new SomeFunction((a, b) =>
    {
       return ((Func<int, float, bool>)action)(a, b);
    });
});

以上面 Hotfix 工程中监听安装为例,onClick 监听其实是基于 UnityAction 来实现的,这就是一个委托,其定义如下:

namespace UnityEngine.Events
{
    //
    // 摘要:
    //     Zero argument delegate used by UnityEvents.
    public delegate void UnityAction();
}

那么在 Unity 工程就需要注册此委托的转化器:

appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((act) =>
{
    return new UnityEngine.Events.UnityAction(() =>
    {
        ((Action)act)();
    });
});

当然,假如忘记注册委托的转化器,运行 Unity 工程便会报错如下,根据报错来补全代码也可以:

KeyNotFoundException: Cannot find convertor for UnityEngine.Events.UnityAction
Please add following code:
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((act) =>
{
    return new UnityEngine.Events.UnityAction(() =>
    {
        ((Action<>)act)();
    });
});

官方的建议:

  • 尽量 避免不必要的 跨域委托调用
  • 尽量使用 Action 以及 Func 这两个系统内置万用委托类型

跨域继承

假如想在 Hotfix 工程中继承 Unity 工程中的一个类,或者实现 Unity 工程中的一个接口,需要在 Unity 工程中实现一个 继承适配器 。官方 Demo 工程提供了三个适配器例子:InheritanceAdapter、CoroutineAdapter 和 MonoBehaviourAdapter,适配器都是继承自 CrossBindingAdaptor 的类,其中有内部类、继承和实现接口的方法。适配器类以下有几点要求:

  • 适配器必须实现抽象类 CrossBindingAdaptor 中的三个接口:
    BaseCLRType 、AdaptorType 和 CreateCLRInstance
public override Type BaseCLRType{
    get { return typeof (继承类); }
}
public override Type AdaptorType{
    get { return typeof (Adaptor); }
}
public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance){
    return new Adaptor(appdomain, instance);
}
  • 内部类继承自你想要提供给 Hotfix 中继承的类,且需要实现 CrossBindingAdaptorType 接口:
    提供与上面 CreateCLRInstance 实例化对象对应的构造方法和 ILInstance 接口
public class Adaptor : 继承类, CrossBindingAdaptorType
{
    ILTypeInstance instance;
    ILRuntime.Runtime.Enviorment.AppDomain appdomain;
    public Adaptor()
    {

    }
    public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
    {
        this.appdomain = appdomain;
        this.instance = instance;
    }
    public ILTypeInstance ILInstance {get { return instance; } }
}
  • 剩下的就是在内部类中重写所有需要暴露给 Hotfix 工程使用的接口:
    下面是实现接口方法和重写虚函数和抽象函数的大致逻辑
IMethod m继承类的虚函数名;
// 虚函数是否在调用中标识
bool is继承类的虚函数名Invoking = false;
IMethod m实现接口的方法名;
object[] param1 = new object[继承类的虚函数名参数数量];
object[] param2 = new object[实现接口方法的参数数量];

// 重写虚函数
public override void 继承类的虚函数名(参数表){
    if(m继承类的虚函数名 == null){
        m继承类的虚函数名 = instance.Type.GetMethod("继承类的虚函数名", 继承类的虚函数名参数数量);
    }
    if(m继承类的虚函数名 != null&& !is继承类的虚函数名Invoking){
        is继承类的虚函数名Invoking = true;
        // param1 传入参数表内容
        param1[0] = 参数表[0];
        ...
        this.appdomain.Invoke(m继承类的虚函数名, instance, this.param1);
        is继承类的虚函数名Invoking = false;
    }else{
        base.继承类的虚函数名(参数表);
    }
}

// 实现接口
public void 实现接口的方法名(参数表){
    if(m实现接口的方法名 == null){
        m实现接口的方法名 = instance.Type.GetMethod("实现接口的方法名", 实现接口方法的参数数量);
    }
    if(m实现接口的方法名 != null){
        // param1 传入参数表内容
        param2[0] = 参数表[0];
        ...
        this.appdomain.Invoke(m实现接口的方法名, instance, this.param2);
    }
}

// 重写抽象函数
public override void 继承类的抽象方法名(参数表){
    // 基本与实现接口一样,多个 override 关键字而已
    ...
}

需要特别注意的细节点 :

  • 没有参数建议显式传递 null 为参数列表,否则会自动 new object[0] 导致 GC Alloc
  • 对于虚函数而言,必须设定一个标识位来确定是否当前已经在调用中,否则如果脚本类中调用 base.继承类的方法名() 就会造成无限循环,最终导致爆栈

更多细节查看官网文档 ILRuntime中跨域继承

为什么要写适配器?

  • ILRuntime 其实是一个独立的 C# 虚拟机,而这个虚拟机要在运行时与 Unity 的脚本进行交互,但由于 iOS 的 AOT 限制,在运行时 ILRuntime 中不知道 Unity 中的类型,所以需要在 Unity 工程中写适配器来让 ILRuntime 知道如何调用 Unity 代码,或当 Unity 的事件触发时让 ILRuntime 能够监听到。

CLR 绑定

在 Hotfix 工程中,假如需要调用 Unity 工程的方法,ILRuntime 会通过反射对目标方法进行调用,这个过程会有因为装箱和拆箱等操作产生的大量 GC Alloc 和额外开销。因此需要借助 CLR 绑定 功能,通过将需要的函数调用进行静态绑定,如此调用时就不会出现 GC Alloc 和额外开销。

绑定代码可以通过 ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode 工具来自动生成。根据官网 Unity Demo 中的 ILRuntimeDemo/ILRuntimeCLRBinding.cs 脚本,通过两种方式来生成:

- 自定义需要生成绑定代码的类型列表(即热更工程可能需要用到的类),传入 GenerateBindingCode
- 分析 Hotfix 工程生成的 dll ,自动分析其中引用到的类型(只会得到已使用的类)

在 Unity 工程中初始化 ILRuntime 的 AppDomain 对象时,调用 CLRBindings.Initialize(appdomain) 完成各个类的 CLR 绑定。假如是值类型,则需要使用 RegisterValueTypeBinder 来绑定:

appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
  • CLR 绑定本质上是基于 CLR 重定向实现的

.dll 和 .pdb

.dll 文件,即 Dynamic Link Library 是动态链接库,.pdb 文件是调试符号(符文表)文件,pdb 保存了 dll 的符号表,文件比较大,程序运行时也会因为要完成映射而比较慢,最后发布 Release 版本或者不需要使用 IDE 进行调试源码的话,没必要引入 .pdb 文件

  • 符号表:是机器码中插入的 key 与源代码文件的映射,这样只要指定源码存放的路径,IDE 就会自动找到源码。
  • dll 和 pdb 是配套的,一旦 dll 文件有变动,pdb 也必须做相应变化。

Unity 工程热更步骤

  • 先从 Hotfix 工程中生成 Hotfix.dll 和 Hotfix.pdb 两个文件

在 VS 2017 中选择 Hotfix 工程,右键 - 生成 ,输出如下:

1>------ 已启动全部重新生成: 项目: Hotfix, 配置: Debug Any CPU ------
1>  Hotfix -> E:\U3DProjects\U3D_TestILRuntime\Hotfix\bin\Debug\Hotfix.dll
========== 全部重新生成: 成功 1 个,失败 0 个,跳过 0 个 ==========

此时,在 Hotfix 工程目录中的 bin/Debug 目录下生成一堆文件,其中就包含 Hotfix.dll 和 Hotfix.pdb

  • 将 Hotfix.dll 和 Hotfix.pdb 两个文件复制到 Unity 工程中的 Assets/StreamingAssets 目录下

  • 在 Unity 工程启动时,通过代码获取热更工程的 .dll 和 .pdb 文件,传给 AppDomain 对象的 LoadAssembly 接口:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class GameMgr : MonoBehaviour
{
    ILRuntime.Runtime.Enviorment.AppDomain appdomain;
    // Start is called before the first frame update
    void Start()
    {
        LoadHotfix();
    }

    async void LoadHotfix(){
        string root = Utils.GetStreamAssetsPath();
        byte [] dllBytes = await Utils.LoadFileBytesAsync(root + "/Hotfix.dll");
        byte [] pdbBytes = await Utils.LoadFileBytesAsync(root + "/Hotfix.pdb");

        if(dllBytes != null && pdbBytes != null){
            Debug.Log("Load Hotfix.dll and Hotfix.pdb success");
            appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
            using (System.IO.MemoryStream fs = new MemoryStream(dllBytes))
            {
                using (System.IO.MemoryStream p = new MemoryStream(pdbBytes))
                {
                    appdomain.LoadAssembly(fs, p, new Mono.Cecil.Pdb.PdbReaderProvider());
                }
            }
            ILRuntimeTest();
        }else{
            if(dllBytes == null){
                Debug.Log("Load Hotfix.dll fail");
            }
            if(pdbBytes == null){
                Debug.Log("Load Hotfix.pdb fail");
            }
        }
    }
    
    // ILRuntime 初始化,主要用于:绑定委托、CLR 绑定和绑定Adapter适配器
    void ILRuntimeInitalize(){
        appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((act) =>
        {
            return new UnityEngine.Events.UnityAction(() =>
            {
                ((Action)act)();
            });
        });
        CLRBindings.Initialize(appdomain);
        appdomain.RegisterCrossBindingAdaptor(new IUIInterfaceAdapter());
    }

    void ILRuntimeTest(){
        Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg", null, null));
        object[] param = new object[1];
        param[0] = 666;
        Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg1", null, param));
        // 创建一个 Test 对象
        var testInst = appdomain.Instantiate<IUIInterface>("Hotfix.Test");
        Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg2", testInst, null));
        testInst.Show();
        Debug.Log(testInst.GetStr());
    }
 
    // Update is called once per frame
    void Update()
    {
        
    }
}
  • 运行 Unity ,点击屏幕中的按钮,可以看到如下输出:


    image.png

iOS IL2CPP 打包

IL2CPP和mono的最大区别就是不能在运行时动态生成代码和类型,所以这就要求必须在编译时就完全确定需要用到的类型。

  • 类型裁剪
    这里主要是 IL2CPP 打包时会对 Unity 工程进行裁剪,裁剪掉其中没有引用到的类型,已达到减小发布后 ipad 包的尺寸。Unity 支持通过在 Assets 目录中创建一个 link.xml 配置文件,来告诉 Unity 那些类型不能被裁剪掉。(工程包体本身较小的可以在 PlayerSettings 中把裁剪直接关掉)例如:
<linker>
  <assembly fullname="UnityEngine" preserve="all"/>
  <assembly fullname="Assembly-CSharp">
    <namespace fullname="MyGame.Utils" preserve="all"/>
    <type fullname="MyGame.SomeClass" preserve="all"/>
  </assembly>  
</linker>
  • 泛型实例和泛型方法

参考 iOS IL2CPP打包注意事项

命令行编译 Hotfix.csproj

每次修改 Hotfix 内容后都要在 VS 2017 中重新生成 Hotfix ,但我习惯使用 VS Code 作为编辑器,想着能不能通过命令行的方式完成 Hotfix 工程的编译工程。大致有两种做法:

  • devenv 是 VS 的可执行程序,一般在 "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE" 目录下,其中 devenv.com 是命令行程序,devenv.exe 是 GUI 的程序
$ devenv Hotfix/Hotfix.vcxproj /Build "Release|Win32"
  • MSBuild 不依赖 VS,是 .NET Framework 安装时自带的工具,可以在路径 "C:\Windows\Microsoft.NET\Framework" 获得,VS 的 devenv 工具做种实现也是调用 MSBuild 来完成的。直接从 v4.0.30319 目录下即可获得 4.5、4.6、4.7 可用的 MSBuild 工具(因为 4.x 其实都是 4.0 的 in place 升级)完整路径为 “C:/Windows/Microsoft.NET/Framework/v4.0.30319/MSBuild.exe” ,将其配置到系统 Path 中,编译命令如下:
$ MSBuild Hotfix/Hotfix.csproj /t:Rebuild /p:Configuration=Release

注意

  • c# 6.0以上可能需要用到"D:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/MSBuild/Current/Bin/MSBuild.exe”这个地址进行编译
    可以直接配置成 VS Code 中的任务:
{
    "version": "2.0.0",
    "inputs": [
        {
            "id": "build",
            "type": "pickString",
            "description": "选择构建类型",
            "options": [
                "Debug",
                "Release"
            ]
        }
    ],
    "tasks": [
        {
            "label": "build hotfix",
            "type": "shell",
            "command": "C:/Windows/Microsoft.NET/Framework/v4.0.30319/MSBuild.exe",
            "args": [
                "${workspaceFolder}/Hotfix/Hotfix.csproj",
                "/t:Rebuild",
                "/p:Configuration=${input:build}"
            ],
            "group": "build",
            "presentation": {
                "reveal": "silent"
            },
            "problemMatcher": "$msCompile"
        }
    ]
}

然后通过 Ctlr + Shift + B 执行任务,可以选择构建 Debug 或 Release。

在 Unity 通过 Editor 工具来执行命令行:

    private const string msbuildExe = "C:/Windows/Microsoft.NET/Framework/v4.0.30319/MSBuild.exe";

    [MenuItem("Tools/ILRuntime/Build Hotfix(Debug)")]
    static void BuildHotfixDebug(){
        BuildHotfix("Debug");
    }

    [MenuItem("Tools/ILRuntime/Build Hotfix(Release)")]
    static void BuildHotfixRelease(){
        BuildHotfix("Release");
    }

    static void BuildHotfix(string _c){
        if(!File.Exists(msbuildExe)){
            UnityEngine.Debug.LogError("找不到 MSBuild 工具");
            return;
        }
        System.IO.DirectoryInfo parent = System.IO.Directory.GetParent(Application.dataPath);
        string projectPath = parent.ToString();
        ProcessCommand(msbuildExe, projectPath + "/Hotfix/Hotfix.csproj /t:Rebuild /p:Configuration=" + _c);
        UnityEngine.Debug.LogFormat("Hotfix {0} 编译完成", _c);
    }

    public static void ProcessCommand(string command, string argument) {
        ProcessStartInfo start = new ProcessStartInfo(command);
        start.Arguments = argument;
        start.CreateNoWindow = true;
        start.ErrorDialog = true;
        start.UseShellExecute = true; 
        if (start.UseShellExecute) {
            start.RedirectStandardOutput = false;
            start.RedirectStandardError = false;
            start.RedirectStandardInput = false;
        } else {
            start.RedirectStandardOutput = true;
            start.RedirectStandardError = true;
            start.RedirectStandardInput = true;
            start.StandardOutputEncoding = System.Text.UTF8Encoding.UTF8;
            start.StandardErrorEncoding = System.Text.UTF8Encoding.UTF8;
        } 
        Process p = Process.Start(start); 
        if (!start.UseShellExecute) {
            UnityEngine.Debug.LogFormat("--- output:{0}", p.StandardOutput.ToString());
            printOutPut(p.StandardOutput);
            printOutPut(p.StandardError);
        } 
        p.WaitForExit();
        p.Close();
    }

ILRuntime 和 Lua 热更方案的优劣

市场上主流的还是 Lua 系,先 tolua 和 xlua 框架在游戏行业基本是了大部分游戏项目的热更选择;C# 系 的成熟方案还是较少。关于两种热更方案的优劣,参考 《必读!ILRuntime来实现热更新的优与劣!》《XLua 与 ILRuntime 性能测试》,主要提到了几点:

  • 不管是 Lua 实现还是 ILRuntime 实现,热更部分的代码都不继承 MonoBehaviour

  • .net4.6 的 async\wait 所支持的现在版本应该也还不够稳定,纯计算的性能弱于 Lua

  • ILRuntime 性能较差,ILRuntime 是自己实现一套解释器,且是用 C# 编写的,原生性能较差。而 Lua 有 Jit ,在支持 Jit 的设备上有接近 c 的性能。

  • ILRuntime 在系统值计算上,由于需要通过 CLR 绑定来在 C# 层面计算,因此性能较差。

其他
测试工程:linshuhe/U3D_TestILRuntime

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容