前言
做游戲離不開熱更新,目前市面上熱更新方案用的比較多的是Lua(XLua,ToLua),最近又出現(xiàn)了基于C#的熱更新 huatuo(已改名HybridCLR又叫wolong)。來不及學習了,以后用到了再去了解吧。
筆者入行做的第一個項目是利用ILRuntime進行熱更新的,當時也是用的稀里糊涂的,一些坑點都是項目主程去解決的。這里做一個簡單的回顧。
一、ILRuntime是什么?
1.官方簡介
ILRuntime項目為基于C#的平臺(例如Unity)提供了一個純C#實現(xiàn),快速、方便且可靠的IL運行時,使得能夠在不支持JIT的硬件環(huán)境(如iOS)能夠?qū)崿F(xiàn)代碼的熱更新。
2.實現(xiàn)原理
ILRuntime借助Mono.Cecil庫來讀取DLL的PE信息,以及當中類型的所有信息,最終得到方法的IL匯編碼,然后通過內(nèi)置的IL解譯執(zhí)行虛擬機來執(zhí)行DLL中的代碼來實現(xiàn)熱更新功能。
查看ILRuntime源碼你會發(fā)現(xiàn),內(nèi)部有一個很大的switch/case結(jié)構(gòu),就是針對基本上每一條IL指令碼進行解釋,同時維護一個Stackframe用于模擬cpu的函數(shù)調(diào)用的基本操作進行輔助解釋。
ILRuntime中解釋熱更dll中的自定義類實例,在框架層這邊都是對應(yīng)的同一個warper,即ILTypeInstance。
ILTypeInstance會知道最終被調(diào)用方法的il指令內(nèi)容,如果調(diào)用,則就是switch逐句去解析這個方法的IL代碼。
這樣一來就沒有什么執(zhí)行權(quán)限的問題,簡單理解為讀取一個普通文件,然后解析文件內(nèi)容。
如果是反射處理這種情形,那就是真實的構(gòu)建出一個新的類型,然后調(diào)用新類型的方法,這倒是會涉及到內(nèi)存權(quán)限問題。
二、ILRuntime使用
當時使用的時候只記得有以下限制:
1.不能手動掛載熱更Mono腳本,只能通過代碼AddComponent
2.不能使用非System.Action/Fun類型的委托,需要手動注冊委托類型轉(zhuǎn)換
3.需要將類的成員初始化賦值刪除,改為在方法內(nèi)初始化
4.不允許使用ref和out
5.將熱更腳本放在特定文件夾。通過定義宏,在日常開發(fā)中在Assembly-CSharp編譯調(diào)用,打包時將熱更腳本單獨打DLL。
當時每天都在趕UI,沒有去細究為什么這么做。
由于項目已經(jīng)掛掉了,這里就不去糾結(jié)了。下面記錄一下自己學習ILRuntime的歷程。
1.跨域委托
只在熱更新的DLL項目中使用的委托,是不需要任何額外操作的,就跟在通常的C#里那樣使用即可。
如果你需要將委托實例傳給ILRuntime外部使用,那則根據(jù)情況,你需要額外添加適配器或者轉(zhuǎn)換器。
示例:同一個參數(shù)組合的委托,只需要注冊一次即可
Action,以及Func委托需要在主工程注冊適配器
// 無返回值委托
appDomain.DelegateManager.RegisterMethodDelegate<int, float>();
// 帶返回值委托
appDomain.DelegateManager.RegisterFunctionDelegate<int, float, bool>();
自定義委托需要額外添加轉(zhuǎn)換器DelegateConvertor
// 自定義委托
delegate bool SomeFunction(int a, float b);
app.DelegateManager.RegisterDelegateConvertor<SomeFunction>((action) =>
{
return new SomeFunction((a, b) =>
{
return ((Func<int, float, bool>)action)(a, b);
});
});
官方建議:
盡量避免不必要的跨域委托調(diào)用。
盡量使用Action以及Func這兩個系統(tǒng)內(nèi)置萬用委托類型。
2.跨域繼承
如果你想在熱更DLL項目當中繼承一個Unity主工程里的類,或者實現(xiàn)一個主工程里的接口,你需要在Unity主工程中實現(xiàn)一個繼承適配器。
為什么需要適配器?
1)防止熱更層用到的框架層代碼被裁減。
為什么會被裁減呢?因為Unity打包的時候真的不把這個熱更dll看做dll,因為這個熱更dll是脫離unity框架層的。自然在unity打包的時候,為了包體大小會把認為沒有使用的代碼全部過濾掉。這種情況下ILRuntime解釋執(zhí)行的時候,去反射調(diào)用框架層代碼就會被視為錯誤,因為框架層不存在這些被調(diào)用的代碼。
因為脫離了關(guān)系,那么如何在框架層中驅(qū)動的時候,可以同步驅(qū)動到熱更層,這就成了一個問題。這就需要框架層引用熱更層的相關(guān)instance去驅(qū)動 ,那么如何引用?這就是適配器的作用。適配器工作在框架層,其顯式強調(diào)了需要引用驅(qū)動的類型實例,然后重寫相關(guān)函數(shù)體內(nèi)容,去實質(zhì)調(diào)用 熱更類型實例 的方法。具體參考MonoBehaviourAdapter即可理解。
ILRuntime提供了一個代碼生成工具來自動生成跨域繼承的適配器代碼。
示例:
void OnHotFixLoaded()
{
Debug.Log("首先我們來創(chuàng)建熱更里的類實例");
TestClassBase obj;
Debug.Log("現(xiàn)在我們來注冊適配器, 該適配器由ILRuntime/Generate Cross Binding Adapter菜單命令自動生成");
appdomain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
Debug.Log("現(xiàn)在再來嘗試創(chuàng)建一個實例");
obj = appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");
Debug.Log("現(xiàn)在來調(diào)用成員方法");
obj.TestAbstract(123);
obj.TestVirtual("Hello");
obj.Value = 233;
Debug.LogFormat("obj.Value={0}", obj.Value);
Debug.Log("現(xiàn)在換個方式創(chuàng)建實例");
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);
}
3.CLR綁定與重定向
為什么需要綁定與重定向機制?
1)防止熱更層用到的框架層代碼被裁減。
2)加速熱更代碼的執(zhí)行。
加速熱更代碼執(zhí)行其實是ILRuntime解釋每條il指令的時候,都會去現(xiàn)有緩存中查找當前指令是否為重定向函數(shù),如果為重定向函數(shù),則直接調(diào)用,如果不是重定向函數(shù),則會反射調(diào)用。通過反射來調(diào)用接口調(diào)用效率會比直接調(diào)用低很多,反射傳遞函數(shù)參數(shù)時需要使用object[]數(shù)組,這樣不可避免的每次調(diào)用都會產(chǎn)生不少GC Alloc。眾所周知GC Alloc高意味著在Unity中執(zhí)行會存在較大的性能問題。
ILRuntime提供了一個代碼生成工具來自動生成CLR綁定代碼。
生成代碼示例:
namespace ILRuntime.Runtime.Generated
{
unsafe class HelloWorld_Binding
{
public static void Register(ILRuntime.Runtime.Enviorment.AppDomain app)
{
BindingFlags flag = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
MethodBase method;
Type[] args;
Type type = typeof(global::HelloWorld);
args = new Type[]{};
method = type.GetMethod("TestHotfixInvokeMain", flag, null, args, null);
app.RegisterCLRMethodRedirection(method, TestHotfixInvokeMain_0);
args = new Type[]{};
method = type.GetConstructor(flag, null, args, null);
app.RegisterCLRMethodRedirection(method, Ctor_0);
}
static StackObject* TestHotfixInvokeMain_0(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* ptr_of_this_method;
StackObject* __ret = ILIntepreter.Minus(__esp, 1);
ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
global::HelloWorld instance_of_this_method = (global::HelloWorld)typeof(global::HelloWorld).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack), (CLR.Utils.Extensions.TypeFlags)0);
__intp.Free(ptr_of_this_method);
instance_of_this_method.TestHotfixInvokeMain();
return __ret;
}
static StackObject* Ctor_0(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* __ret = ILIntepreter.Minus(__esp, 0);
var result_of_this_method = new global::HelloWorld();
return ILIntepreter.PushObject(__ret, __mStack, result_of_this_method);
}
}
}
先學到這,持續(xù)更新中。。。文章來源:http://www.zghlxwxcb.cn/news/detail-404295.html
參考鏈接:
github倉庫地址:https://github.com/Ourpalm/ILRuntime
中文文檔:https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html
使用ILRuntime遇到的一些問題
王王王渣渣ILRuntime系列文章來源地址http://www.zghlxwxcb.cn/news/detail-404295.html
到了這里,關(guān)于Unity 熱更新方案之——ILRuntime的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!