Unity 接入 ILRuntime 熱更方案

引言

最近看了一下 ET 框架,本來只是研究一下網(wǎng)絡(luò)模塊,后來抽時間看一下熱更框架此虑。ET 的熱更使用的不是像 tolua 這樣基于 Lua 的方案,而是基于 ILRuntime 的純 C# 熱更實現(xiàn)方案口锭。

ILRuntime 的實現(xiàn)原理

對 Unity 引擎有一定了解的開發(fā)者都應(yīng)該知道: Unity 支持使用 C# 做腳本語言寡壮,是依賴 Mono 引擎運行 C# 編譯后的 IL 中間語言。ILRuntime 借助 Mono.Cecil 庫來讀取 DLL 的 PE 信息,以及當(dāng)中類型的所有信息况既,最終得到方法的 IL 匯編碼这溅,然后通過內(nèi)置的 IL 解譯執(zhí)行虛擬機來執(zhí)行 DLL 中的代碼。

Cecil 是一個用來生成(修改和創(chuàng)建)和檢查 ECMA CIL 格式的程序和庫的庫棒仍,可以完成如下操作:

使用簡單而強大的對象模型分析 .NET 二進制文件悲靴,無需通過加載程序集即可使用 Reflection(反射)

修改 .NET 二進制文件,添加新的元數(shù)據(jù)結(jié)構(gòu)并更改 IL 代碼

Cecil 官網(wǎng): http://cecil.pe

相關(guān)資源

ILRuntime 是 2016 年發(fā)布的一個開源項目莫其,2017 年發(fā)布了第一個正式版癞尚,地址 Ourpalm/ILRuntime

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

官方提供的 Unity Demo

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

教程
從零開始
ILRuntime中使用委托
ILRuntime中跨域繼承
ILRuntime中的反射
CLR重定向
CLR綁定
LitJson集成

其他
IL2CPP打包注意事項
ILRuntime的性能優(yōu)化
ILRuntime的實現(xiàn)原理

Unity 集成步驟

參考官方文檔 從零開始,基本就一下幾個步驟:

  • 下載最新的 release 版本 ILRuntime-1.4.zip 乱陡,然后解壓縮
  • 將 ILRuntime 源碼工程下的 Mono.Cecil.20 浇揩、Mono.Cecil.Pdb 和 ILRuntime 復(fù)制到 Unity 工程 Assets 目錄下
需刪除這些目錄下的 bin 、obj 和 Properties 子目錄憨颠,還有 .csproj 文件
  • Unity 開啟 unsafe 模式
    • 在 Assets 目錄下創(chuàng)建一個命名為 smcs.rsp 文本文件胳徽,內(nèi)容為 -unsafe
Unity5.4 及以前的版本,且編譯設(shè)置是 .Net 2.0 而不是 .Net 2.0 Subset 的話爽彤, 需要將 smcs.rsp 文件名改成 gmcs.rsp
Unity5.5 以上的版本养盗,需要將 smcs.rsp 文件名改成 mcs.rsp
  • 創(chuàng)建熱更工程(Hotfix)

熱更工程是一個獨立于 Unity 工程的一個獨立的 C# 類庫工程,這里需用借助 VS 2017 來完成創(chuàng)建操作适篙,創(chuàng)建步驟如下:

  • 使用 VS 2017 打開當(dāng)前 Unity 工程的 .sln 文件
  • 文件 - 新建 - 項目 往核,選擇 Visual C# 欄 中的 類庫(.NET Framework) ,然后完成剩余的設(shè)置:
1.命名為 Hotfix 
2.位置是 Unity 工程根目錄
3.解決方案為 添加到解決方案 (即添加到當(dāng)前 Unity 的解決方案內(nèi))
4.框架設(shè)置為與 Unity 工程版本一樣的 .NET Framwork 4.7.1(根據(jù)自己項目情況選擇)
image.png

然后點擊 確定 創(chuàng)建出 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 工程的業(yè)務(wù)代碼庫

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

到這里 Hotfix 工程就創(chuàng)建成功了衩婚,下面便是具體的測試代碼。

測試代碼

這個要測試兩個方面碍论,一是在 Hotfix 工程中調(diào)用 Unity 的接口谅猾,二是在 Unity 工程中調(diào)用 Hotfix 提供的接口。這里參考 ILRuntime中的反射 文檔即可實現(xiàn)鳍悠,具體如下:

  • 創(chuàng)建場景

在 Unity 工程中創(chuàng)建一個空場景税娜,添加一個 UI 相機、Canvas 和 一個測試按鈕藏研,結(jié)構(gòu)如下:


image.png

從 Unity 中調(diào)用 Hotfix 提供的靜態(tài)方法和非靜態(tài)方法:

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;
        }
        // 非靜態(tài)
        public String GetMsg2(){
            Debug.Log("call GetMsg2");
            return "Test Hotfix, no static";
        }
    }
}

Unity 工程中調(diào)用的邏輯如下:

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));
    // 創(chuàng)建一個 Test 對象
    var testInst = appdomain.Instantiate("Hotfix.Test");
    Debug.Log(appdomain.Invoke("Hotfix.Test", "GetMsg2", testInst, null));
}

這里 appdomain 是一個 ILRuntime.Runtime.Enviorment.AppDomain 實例對象敬矩,需要加載 Hotfix.dll 后才能開始執(zhí)行上述的測試方法。

  • 從 Unity 提供給 Hotfix 調(diào)用的靜態(tài)和非靜態(tài)方法:
    • 調(diào)用 Unity 工程中的類方法
      上面的測試代碼其實用到了 Unity 的 Engine.Debug.Log 接口蠢挡,基本是直接調(diào)用 Unity 工程中的類方法弧岳,但
      這樣存在性能問題凳忙,后面通過 CLR 綁定來優(yōu)化性能。

    • 繼承 Unity 工程中的類或?qū)崿F(xiàn)接口
      假如需要在 Hotfix 工程中繼承 Unity 工程中的類或?qū)崿F(xiàn)接口禽炬,則需要在 Unity 工程中增加對應(yīng)類或接口的
      適配器涧卵。

    • 使用 Unity 中的值類型,如:Vector3腹尖、Vector2 等
      也是可以直接調(diào)用柳恐,但也需要使用 CLR 綁定來做性能優(yōu)化。

    • Hotfix 工程中使用委托
      假如只是 Hotfix 工程內(nèi)部使用的委托热幔,無需做任何額外操作(因為委托是 C# 的特性乐设,而非 Unity 的)。
      但假如需要將 Hotfix 工程中的委托實例傳給 Unity 工程绎巨,也需要根據(jù)情況添加額外的適配器和轉(zhuǎn)化器近尚。

這里在 Unity 工程中定義一個 IUIBase 的接口:

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

對應(yīng)的 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 類實現(xiàn)此接口:

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());

創(chuàng)建 Test 對象并調(diào)用接口方法:

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

委托

這里主要分析將 Hotfix 工程內(nèi)的委托實例傳給外部使用的情況,此時需要將委托實例轉(zhuǎn)換成真正的 CLR(C#運行時)委托實例场勤,即通過動態(tài)創(chuàng)建 CLR 的委托實例戈锻。由于 IL2CPP 之類的 AOT 編譯技術(shù)無法在運行時生成新的類型,所以在創(chuàng)建委托實例的時候 ILRuntime 選擇了顯示注冊的方式却嗡,以保證問題不被隱藏到線上才發(fā)現(xiàn)舶沛。

  • 委托適配器:
    參數(shù)組合一致的各種 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>();
  • 委托轉(zhuǎn)換器:
    ILRuntime 內(nèi)是使用 Action 和 Func 兩個系統(tǒng)自帶的委托類型來生成委托實例的,因此如果在 Hotfix 工程中用到的非 Action 和 Func 格式定義的委托實例要傳給 Unity 工程中使用叹卷,需要在注冊委托的地方通過轉(zhuǎn)換器轉(zhuǎn)成真正需要的委托類型:
app.DelegateManager.RegisterDelegateConvertor<SomeFunction>((action) =>
{
    return new SomeFunction((a, b) =>
    {
       return ((Func<int, float, bool>)action)(a, b);
    });
});

以上面 Hotfix 工程中監(jiān)聽安裝為例撼港,onClick 監(jiān)聽其實是基于 UnityAction 來實現(xiàn)的,這就是一個委托骤竹,其定義如下:

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

那么在 Unity 工程就需要注冊此委托的轉(zhuǎn)化器:

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

當(dāng)然帝牡,假如忘記注冊委托的轉(zhuǎn)化器,運行 Unity 工程便會報錯如下蒙揣,根據(jù)報錯來補全代碼也可以:

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)();
    });
});

官方的建議:

  • 盡量 避免不必要的 跨域委托調(diào)用
  • 盡量使用 Action 以及 Func 這兩個系統(tǒng)內(nèi)置萬用委托類型

跨域繼承

假如想在 Hotfix 工程中繼承 Unity 工程中的一個類靶溜,或者實現(xiàn) Unity 工程中的一個接口,需要在 Unity 工程中實現(xiàn)一個 繼承適配器 懒震。官方 Demo 工程提供了三個適配器例子:InheritanceAdapter罩息、CoroutineAdapter 和 MonoBehaviourAdapter,適配器都是繼承自 CrossBindingAdaptor 的類个扰,其中有內(nèi)部類瓷炮、繼承和實現(xiàn)接口的方法。適配器類以下有幾點要求:

  • 適配器必須實現(xiàn)抽象類 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);
}
  • 內(nèi)部類繼承自你想要提供給 Hotfix 中繼承的類娘香,且需要實現(xiàn) CrossBindingAdaptorType 接口:
    提供與上面 CreateCLRInstance 實例化對象對應(yīng)的構(gòu)造方法和 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; } }
}
  • 剩下的就是在內(nèi)部類中重寫所有需要暴露給 Hotfix 工程使用的接口:
    下面是實現(xiàn)接口方法和重寫虛函數(shù)和抽象函數(shù)的大致邏輯
IMethod m繼承類的虛函數(shù)名;
// 虛函數(shù)是否在調(diào)用中標(biāo)識
bool is繼承類的虛函數(shù)名Invoking = false;
IMethod m實現(xiàn)接口的方法名;
object[] param1 = new object[繼承類的虛函數(shù)名參數(shù)數(shù)量];
object[] param2 = new object[實現(xiàn)接口方法的參數(shù)數(shù)量];

// 重寫虛函數(shù)
public override void 繼承類的虛函數(shù)名(參數(shù)表){
    if(m繼承類的虛函數(shù)名 == null){
        m繼承類的虛函數(shù)名 = instance.Type.GetMethod("繼承類的虛函數(shù)名", 繼承類的虛函數(shù)名參數(shù)數(shù)量);
    }
    if(m繼承類的虛函數(shù)名 != null&& !is繼承類的虛函數(shù)名Invoking){
        is繼承類的虛函數(shù)名Invoking = true;
        // param1 傳入?yún)?shù)表內(nèi)容
        param1[0] = 參數(shù)表[0];
        ...
        this.appdomain.Invoke(m繼承類的虛函數(shù)名, instance, this.param1);
        is繼承類的虛函數(shù)名Invoking = false;
    }else{
        base.繼承類的虛函數(shù)名(參數(shù)表);
    }
}

// 實現(xiàn)接口
public void 實現(xiàn)接口的方法名(參數(shù)表){
    if(m實現(xiàn)接口的方法名 == null){
        m實現(xiàn)接口的方法名 = instance.Type.GetMethod("實現(xiàn)接口的方法名", 實現(xiàn)接口方法的參數(shù)數(shù)量);
    }
    if(m實現(xiàn)接口的方法名 != null){
        // param1 傳入?yún)?shù)表內(nèi)容
        param2[0] = 參數(shù)表[0];
        ...
        this.appdomain.Invoke(m實現(xiàn)接口的方法名, instance, this.param2);
    }
}

// 重寫抽象函數(shù)
public override void 繼承類的抽象方法名(參數(shù)表){
    // 基本與實現(xiàn)接口一樣苍狰,多個 override 關(guān)鍵字而已
    ...
}

需要特別注意的細節(jié)點 :

  • 沒有參數(shù)建議顯式傳遞 null 為參數(shù)列表,否則會自動 new object[0] 導(dǎo)致 GC Alloc
  • 對于虛函數(shù)而言烘绽,必須設(shè)定一個標(biāo)識位來確定是否當(dāng)前已經(jīng)在調(diào)用中淋昭,否則如果腳本類中調(diào)用 base.繼承類的方法名() 就會造成無限循環(huán),最終導(dǎo)致爆棧

更多細節(jié)查看官網(wǎng)文檔 ILRuntime中跨域繼承

為什么要寫適配器安接?

  • ILRuntime 其實是一個獨立的 C# 虛擬機响牛,而這個虛擬機要在運行時與 Unity 的腳本進行交互,但由于 iOS 的 AOT 限制赫段,在運行時 ILRuntime 中不知道 Unity 中的類型呀打,所以需要在 Unity 工程中寫適配器來讓 ILRuntime 知道如何調(diào)用 Unity 代碼,或當(dāng) Unity 的事件觸發(fā)時讓 ILRuntime 能夠監(jiān)聽到糯笙。

CLR 綁定

在 Hotfix 工程中贬丛,假如需要調(diào)用 Unity 工程的方法,ILRuntime 會通過反射對目標(biāo)方法進行調(diào)用给涕,這個過程會有因為裝箱和拆箱等操作產(chǎn)生的大量 GC Alloc 和額外開銷豺憔。因此需要借助 CLR 綁定 功能,通過將需要的函數(shù)調(diào)用進行靜態(tài)綁定够庙,如此調(diào)用時就不會出現(xiàn) GC Alloc 和額外開銷恭应。

綁定代碼可以通過 ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode 工具來自動生成。根據(jù)官網(wǎng) Unity Demo 中的 ILRuntimeDemo/ILRuntimeCLRBinding.cs 腳本耘眨,通過兩種方式來生成:

- 自定義需要生成綁定代碼的類型列表(即熱更工程可能需要用到的類)昼榛,傳入 GenerateBindingCode
- 分析 Hotfix 工程生成的 dll ,自動分析其中引用到的類型(只會得到已使用的類)

在 Unity 工程中初始化 ILRuntime 的 AppDomain 對象時剔难,調(diào)用 CLRBindings.Initialize(appdomain) 完成各個類的 CLR 綁定胆屿。假如是值類型,則需要使用 RegisterValueTypeBinder 來綁定:

appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
  • CLR 綁定本質(zhì)上是基于 CLR 重定向?qū)崿F(xiàn)的

.dll 和 .pdb

.dll 文件偶宫,即 Dynamic Link Library 是動態(tài)鏈接庫非迹,.pdb 文件是調(diào)試符號(符文表)文件,pdb 保存了 dll 的符號表,文件比較大,程序運行時也會因為要完成映射而比較慢麦撵,最后發(fā)布 Release 版本或者不需要使用 IDE 進行調(diào)試源碼的話,沒必要引入 .pdb 文件

  • 符號表:是機器碼中插入的 key 與源代碼文件的映射纯命,這樣只要指定源碼存放的路徑,IDE 就會自動找到源碼桦锄。
  • dll 和 pdb 是配套的扎附,一旦 dll 文件有變動,pdb 也必須做相應(yīng)變化结耀。

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 兩個文件復(fù)制到 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));
        // 創(chuàng)建一個 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的最大區(qū)別就是不能在運行時動態(tài)生成代碼和類型潮秘,所以這就要求必須在編譯時就完全確定需要用到的類型琼开。

  • 類型裁剪
    這里主要是 IL2CPP 打包時會對 Unity 工程進行裁剪,裁剪掉其中沒有引用到的類型枕荞,已達到減小發(fā)布后 ipad 包的尺寸柜候。Unity 支持通過在 Assets 目錄中創(chuàng)建一個 link.xml 配置文件,來告訴 Unity 那些類型不能被裁剪掉躏精。(工程包體本身較小的可以在 PlayerSettings 中把裁剪直接關(guān)掉)例如:
<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 內(nèi)容后都要在 VS 2017 中重新生成 Hotfix 渣刷,但我習(xí)慣使用 VS Code 作為編輯器,想著能不能通過命令行的方式完成 Hotfix 工程的編譯工程矗烛。大致有兩種做法:

  • devenv 是 VS 的可執(zhí)行程序辅柴,一般在 "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 工具做種實現(xiàn)也是調(diào)用 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” ,將其配置到系統(tǒng) 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 中的任務(wù):
{
    "version": "2.0.0",
    "inputs": [
        {
            "id": "build",
            "type": "pickString",
            "description": "選擇構(gòu)建類型",
            "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 執(zhí)行任務(wù)惠呼,可以選擇構(gòu)建 Debug 或 Release。

在 Unity 通過 Editor 工具來執(zhí)行命令行:

    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 熱更方案的優(yōu)劣

市場上主流的還是 Lua 系峦耘,先 tolua 和 xlua 框架在游戲行業(yè)基本是了大部分游戲項目的熱更選擇剔蹋;C# 系 的成熟方案還是較少。關(guān)于兩種熱更方案的優(yōu)劣辅髓,參考 《必讀泣崩!ILRuntime來實現(xiàn)熱更新的優(yōu)與劣少梁!》《XLua 與 ILRuntime 性能測試》,主要提到了幾點:

  • 不管是 Lua 實現(xiàn)還是 ILRuntime 實現(xiàn)矫付,熱更部分的代碼都不繼承 MonoBehaviour

  • .net4.6 的 async\wait 所支持的現(xiàn)在版本應(yīng)該也還不夠穩(wěn)定凯沪,純計算的性能弱于 Lua

  • ILRuntime 性能較差,ILRuntime 是自己實現(xiàn)一套解釋器买优,且是用 C# 編寫的妨马,原生性能較差。而 Lua 有 Jit 杀赢,在支持 Jit 的設(shè)備上有接近 c 的性能烘跺。

  • ILRuntime 在系統(tǒng)值計算上,由于需要通過 CLR 綁定來在 C# 層面計算脂崔,因此性能較差液荸。

其他
測試工程:linshuhe/U3D_TestILRuntime

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脱篙,一起剝皮案震驚了整個濱河市娇钱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绊困,老刑警劉巖文搂,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秤朗,居然都是意外死亡煤蹭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門取视,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硝皂,“玉大人,你說我怎么就攤上這事作谭』铮” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵折欠,是天一觀的道長贝或。 經(jīng)常有香客問我,道長锐秦,這世上最難降的妖魔是什么咪奖? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮酱床,結(jié)果婚禮上羊赵,老公的妹妹穿的比我還像新娘。我一直安慰自己扇谣,他們只是感情好昧捷,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布揖闸。 她就那樣靜靜地躺著,像睡著了一般料身。 火紅的嫁衣襯著肌膚如雪汤纸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天芹血,我揣著相機與錄音贮泞,去河邊找鬼。 笑死幔烛,一個胖子當(dāng)著我的面吹牛啃擦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饿悬,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼令蛉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了狡恬?” 一聲冷哼從身側(cè)響起珠叔,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弟劲,沒想到半個月后祷安,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡兔乞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年汇鞭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庸追。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡霍骄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出淡溯,到底是詐尸還是另有隱情读整,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布血筑,位于F島的核電站绘沉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏豺总。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一择懂、第九天 我趴在偏房一處隱蔽的房頂上張望喻喳。 院中可真熱鬧,春花似錦困曙、人聲如沸表伦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹦哼。三九已至鳄哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纲熏,已是汗流浹背妆丘。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留局劲,地道東北人勺拣。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像鱼填,于是被迫代替她去往敵國和親药有。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容