ILRuntime熱更案例學習(三) ------ Reflection/CLRBinding/CLRRedirection
官方示例下載地址1: https://github.com/Ourpalm/ILRuntime
官方示例下載地址2 : https://github.com/Ourpalm/ILRuntimeU3D
官方文件地址 : https://ourpalm.github.io/ILRuntime/public/v1/guide/tutorial.html
一.Reflection
void OnHotFixLoaded() { var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"]; Debug.Log("LoadedTypes返回的是IType型別,但是我們需要獲得對應的System.Type才能繼續使用反射介面"); var type = it.ReflectionType; 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)); }
在熱更DLL中使用反射跟原生C#沒有任何區別,在主工程中反射熱更DLL中的型別均需要通過AppDomain取得
1.反射獲取型別
在熱更DLL
當中:
//在熱更DLL中,以下兩種方式均可以
Type t = typeof(TypeName);
Type t2 = Type.GetType("TypeName");
在Unity主工程中:
IType type = appdomain.LoadedTypes["TypeName"];
Type t = type.ReflectedType;
2.反射建立例項
在熱更DLL
當中:
Type t = Type.GetType("TypeName");//或者typeof(TypeName) //以下兩種方式均可以 object instance = Activator.CreateInstance(t); object instance = Activator.CreateInstance<TypeName>();
在Unity主工程中:
object instance = appdomain.Instantiate("TypeName");
3.反射呼叫方法
在熱更DLL
當中:
Type type = typeof(TypeName);
object instance = Activator.CreateInstance(type);
MethodInfo mi = type.GetMethod("foo");
mi.Invoke(instance, null);
在Unity主工程中:
IType t = appdomain.LoadedTypes["TypeName"]; Type type = t.ReflectedType; object instance = appdomain.Instantiate("TypeName"); //系統反射介面 MethodInfo mi = type.GetMethod("foo"); mi.Invoke(instance, null); //ILRuntime的介面 IMethod m = t.GetMethod("foo", 0); appdomain.Invoke(m, instance, null);
4. 反射獲取和設定Field的值(欄位)
都一樣:
Type t;
FieldInfo fi = t.GetField("field");
object val = fi.GetValue(instance);
fi.SetValue(instance, val);
5.反射獲取Attribute標註
Type t;
FieldInfo fi = t.GetField("field");
object[] attributeArr = fi.GetCustomAttributes(typeof(SomeAttribute), false);
在Unity主工程中不能通過new T()的方式來建立熱更工程中的型別例項
二.CLRBinding(從熱更DLL中呼叫Unity主工程或者Unity的介面)
ILRuntime通過CLR方法繫結機制,可以選擇性
的對經常使用的CLR介面進行直接呼叫,從而儘可能的消除反射呼叫開銷以及額外的GC Alloc
①.找到GenerateCLRBinding方法,將你要被呼叫的類或者介面加入到Type中 :
public class CLRBindingTestClass
{
public static float DoSomeTest(int a, float b)
{
return a + b;
}
}
static void GenerateCLRBinding()
{
List<Type> types = new List<Type>();
types.Add(typeof(int));
types.Add(typeof(float));
types.Add(typeof(long));
types.Add(typeof(object));
types.Add(typeof(string));
types.Add(typeof(Array));
types.Add(typeof(Vector2));
types.Add(typeof(Vector3));
types.Add(typeof(Quaternion));
types.Add(typeof(GameObject));
types.Add(typeof(UnityEngine.Object));
types.Add(typeof(Transform));
types.Add(typeof(RectTransform));
types.Add(typeof(CLRBindingTestClass)); //dll要訪問的類
types.Add(typeof(Time));
types.Add(typeof(Debug));
types.Add(typeof(Dictionary<string, int>));
//所有DLL內的型別的真實C#型別都是ILTypeInstance
types.Add(typeof(List<ILRuntime.Runtime.Intepreter.ILTypeInstance>));
ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(types, "Assets/ILRuntime/Generated");
}
②.重新生成CLR繫結程式碼
生成完成可以看到 :
③.測試
ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);
var type = appdomain.LoadedTypes["HotFix_Project.TestCLRBinding"];
var m = type.GetMethod("RunTest", 0);
dll中的RunTest方法:
public static void RunTest()
{
for (int i = 0; i < 100000; i++)
{
CLRBindingTestClass.DoSomeTest(i, i);
}
}
除此之外在ValueTypeBinding案例中演示了另一種方法實現繫結 : 手動編寫CLR重定向方法,不過特別麻煩不推薦使用,這裡不描述
三.CLRRedirection(重定向)
CLR : 通用語言執行平臺(Common Language Runtime)
在反射中我們可以知道一些依賴反射的介面是沒有辦法直接執行的,最典型的就是在Unity主工程中通過new T()建立熱更DLL內型別的例項。ILRuntime為了解決這類問題,引入了CLR重定向機制。 原理就是當IL解譯器發現需要呼叫某個指定CLR方法時,將實際呼叫重定向到另外一個方法進行挾持,再在這個方法中對ILRuntime的反射的用法進行處理
1.Activator.CreateInstance的CLR重定向(不帶引數)
unsafe void OnHotFixLoaded()
{
foreach (var i in typeof(System.Activator).GetMethods())
{
//找到名字為CreateInstance,並且是泛型方法的方法定義
if (i.Name == "CreateInstance" && i.IsGenericMethodDefinition)
{
appdomain.RegisterCLRMethodRedirection(i, CreateInstance);
}
}
}
public static StackObject* CreateInstance(ILIntepreter intp, StackObject* esp, List<object> mStack, CLRMethod method, bool isNewObj)
{
//獲取泛型引數<T>的實際型別
IType[] genericArguments = method.GenericArguments;
if (genericArguments != null && genericArguments.Length == 1)
{
var t = genericArguments[0];
if (t is ILType)//如果T是熱更DLL裡的型別
{
//通過ILRuntime的介面來建立例項
return ILIntepreter.PushObject(esp, mStack, ((ILType)t).Instantiate());
}
else
return ILIntepreter.PushObject(esp, mStack, Activator.CreateInstance(t.TypeForCLR));//通過系統反射介面建立例項
}
else
throw new EntryPointNotFoundException();
}
2.Unity的Debug.Log介面重定向(帶引數)
unsafe void OnHotFixLoaded()
{
Debug.Log("下面介紹一個CLR重定向的典型用法,比如我們在DLL裡呼叫Debug.Log,預設情況下是無法顯示DLL內堆疊的,像下面這樣");
Debug.Log("但是經過CLR重定向之後可以做到輸出DLL內堆疊,接下來進行CLR重定向註冊");
var mi = typeof(Debug).GetMethod("Log", new System.Type[] { typeof(object) });
appdomain.RegisterCLRMethodRedirection(mi, Log_11);
//這個只是為了演示加的,平時不要這麼用,直接在InitializeILRuntime方法裡面寫CLR重定向註冊就行了
Debug.Log("我們再來呼叫一次剛剛的方法,注意看下一行日誌的變化");
appdomain.Invoke("HotFix_Project.TestCLRRedirection", "RunTest", null, null);
}
//編寫重定向方法對於剛接觸ILRuntime的朋友可能比較困難,比較簡單的方式是通過CLR繫結生成繫結程式碼,然後在這個基礎上改,比如下面這個程式碼是從UnityEngine_Debug_Binding裡面複製來改的
//如何使用CLR繫結請看相關教程和文件
unsafe static StackObject* Log_11(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
//ILRuntime的呼叫約定為被呼叫者清理堆疊,因此執行這個函式後需要將引數從堆疊清理乾淨,並把返回值放在棧頂,具體請看ILRuntime實現原理文件
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
StackObject* ptr_of_this_method;
//這個是最後方法返回後esp棧指標的值,應該返回清理完引數並指向返回值,這裡是只需要返回清理完引數的值即可
StackObject* __ret = ILIntepreter.Minus(__esp, 1);
//取Log方法的引數,如果有兩個引數的話,第一個引數是esp - 2,第二個引數是esp -1, 因為Mono的bug,直接-2值會錯誤,所以要呼叫ILIntepreter.Minus
ptr_of_this_method = ILIntepreter.Minus(__esp, 1);
//這裡是將棧指標上的值轉換成object,如果是基礎型別可直接通過ptr->Value和ptr->ValueLow訪問到值,具體請看ILRuntime實現原理文件
object message = typeof(object).CheckCLRTypes(StackObject.ToObject(ptr_of_this_method, __domain, __mStack));
//所有非基礎型別都得呼叫Free來釋放託管堆疊
__intp.Free(ptr_of_this_method);
//在真實呼叫Debug.Log前,我們先獲取DLL內的堆疊
var stacktrace = __domain.DebugService.GetStackTrance(__intp);
//我們在輸出資訊後面加上DLL堆疊
UnityEngine.Debug.Log(message + "\n" + stacktrace);
return __ret;
}