1. 程式人生 > >ILRuntime熱更案例學習(三) ------ Reflection/CLRBinding/CLRRedirection

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