引言
最近看了一下 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ù)自己項目情況選擇)
然后點擊 確定 創(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
到這里 Hotfix 工程就創(chuàng)建成功了衩婚,下面便是具體的測試代碼。
測試代碼
這個要測試兩個方面碍论,一是在 Hotfix 工程中調(diào)用 Unity 的接口谅猾,二是在 Unity 工程中調(diào)用 Hotfix 提供的接口。這里參考 ILRuntime中的反射 文檔即可實現(xiàn)鳍悠,具體如下:
- 創(chuàng)建場景
在 Unity 工程中創(chuàng)建一個空場景税娜,添加一個 UI 相機、Canvas 和 一個測試按鈕藏研,結(jié)構(gòu)如下:
從 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 愿卒,點擊屏幕中的按鈕,可以看到如下輸出:
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>
- 泛型實例和泛型方法
命令行編譯 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
參考