ILRuntime熱更新 Unity的C#熱更新方案(2018)


1 熱更新方案總結

https://gameinstitute.qq.com/community/detail/119660

2 ILRuntime熱更新Demo

https://github.com/Ourpalm/ILRuntimeU3D

3 ILRuntime官網

http://ourpalm.github.io/ILRuntime/public/v1/guide/index.html

4 ILRuntime和xlua等方案比較

 http://dy.163.com/v2/article/detail/DD9LQ1AJ0511L9VL.html

5 ILRuntime Git

https://github.com/Ourpalm/ILRuntime

 

Git上面的Demo是用2019實現的。2018以下需要自己手動拉取git源碼。再按照說明裝入整合。

 

在Unity中使用Github的master分支

如果你希望在Unity中使用ILRuntime的最新master版本
你需要將下列源碼目錄復制Unity工程的Assets目錄:

  • Dependencies
  • ILRuntime

需要注意的是,需要刪除這些目錄里面的binobjProperties子目錄,以及.csproj文件。此外,由於ILRuntime使用了unsafe代碼來優化執行效率,所以你需要在Unity中開啟unsafe模式:

  • Unity2017以上的版本請在PlayerSettings中勾選Allow unsafe mode
  • Assets目錄里建立一個名為smcs.rsp的文本文件
  • smcs.rsp文件中加入 -unsafe
  • 如果你使用的是Unity5.4及以前的版本,並且使用的編譯設置是.Net 2.0而不是.Net 2.0 Subset的話,你需要將上述說明中的smcs.rsp文件名改成gmcs.rsp
  • 如果你使用的是Unity5.5以上的版本,你需要將上述說明中的smcs.rsp文件名改成mcs.rsp

從Visual Studio開始

如果你希望在VisualStudio的C#項目中使用ILRuntime, 你只需要引用編譯好的ILRuntime.dllILRuntim.Mono.Cecil.dll以及ILRuntime.Mono.Cecil.Pdb.dll即可。

 

整理完畢的項目目錄如下

 

 

 

Dependencies , ILRuntime,LitJson 直接使用從源碼中copy進來。

Example中的cs文件對應ILRuntimeDemo中的各種示例用法。其中都有很詳細的中文注釋。

待Unity項目成功編譯通過生成 Assembly-CSharp.dll

StreamingAssets  中的HotFix_project.dll HotFix_project.pdb 是由另一個HotFix_project項目生成的我們用來熱更新的代碼。 

這里就是我們ILRuntime使用的外殼環境了。

 

熱更新的關鍵代碼就在這里。

//PDB文件是調試數據庫,如需要在日志中顯示報錯的行號,則必須提供PDB文件,不過由於會額外耗用內存,正式發布時請將PDB去掉,下面LoadAssembly的時候pdb傳null即可
#if UNITY_ANDROID
www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif

外殼啟動后動態去加載HotFix_project.。從示例中的各種方法調用其內部。

  void OnHotFixLoaded()
    {
        //HelloWorld,第一次方法調用
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunHelloWorld", null, null);
    }

 

Hotfix_project 項目設置

 

 這里需要引用相關的dll。

UnityEngine就在Hotfix目錄下的/UnityDlls/

Assembly=CSharp是我們外殼環境編譯生成的dll

 

最后將這個項目的編譯生成位置指定到上面代碼加載的 /streamingAssetsPath/

前后貫通 即可使用了。

 

詳細的調用方法

1 靜態方法 ,泛型方法

Debug.Log("調用無參數靜態方法");
        //調用無參數靜態方法,appdomain.Invoke("類名", "方法名", 對象引用, 參數列表);
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
        //調用帶參數的靜態方法
        Debug.Log("調用帶參數的靜態方法");
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);


        Debug.Log("通過IMethod調用方法");
        //預先獲得IMethod,可以減低每次調用查找方法耗用的時間
        IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        //根據方法名稱和參數個數獲取方法
        IMethod method = type.GetMethod("StaticFunTest2", 1);

        appdomain.Invoke(method, null, 123);

        Debug.Log("通過無GC Alloc方式調用方法");
        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushInteger(123);
            ctx.Invoke();
        }

        Debug.Log("指定參數類型來獲得IMethod");
        IType intType = appdomain.GetType(typeof(int));
        //參數類型列表
        List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
        paramList.Add(intType);
        //根據方法名稱和參數類型列表獲取方法
        method = type.GetMethod("StaticFunTest2", paramList, null);
        appdomain.Invoke(method, null, 456);

        Debug.Log("實例化熱更里的類");
        object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });
        //第二種方式
        object obj2 = ((ILType)type).Instantiate();

        Debug.Log("調用成員方法");
        method = type.GetMethod("get_ID", 0);
        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushObject(obj);
            ctx.Invoke();
            int id = ctx.ReadInteger();
            Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
        }

        using (var ctx = appdomain.BeginInvoke(method))
        {
            ctx.PushObject(obj2);
            ctx.Invoke();
            int id = ctx.ReadInteger();
            Debug.Log("!! HotFix_Project.InstanceClass.ID = " + id);
        }
        
        Debug.Log("調用泛型方法");
        IType stringType = appdomain.GetType(typeof(string));
        IType[] genericArguments = new IType[] { stringType };
        appdomain.InvokeGenericMethod("HotFix_Project.InstanceClass", "GenericMethod", genericArguments, null, "TestString");

        Debug.Log("獲取泛型方法的IMethod");
        paramList.Clear();
        paramList.Add(intType);
        genericArguments = new IType[] { intType };
        method = type.GetMethod("GenericMethod", paramList, genericArguments);
        appdomain.Invoke(method, null, 33333);

        Debug.Log("調用帶Ref/Out參數的方法");
        method = type.GetMethod("RefOutMethod", 3);
        int initialVal = 500;
        using(var ctx = appdomain.BeginInvoke(method))
        {
            //第一個ref/out參數初始值
            ctx.PushObject(null);
            //第二個ref/out參數初始值
            ctx.PushInteger(initialVal);
            //壓入this
            ctx.PushObject(obj);
            //壓入參數1:addition
            ctx.PushInteger(100);
            //壓入參數2: lst,由於是ref/out,需要壓引用,這里是引用0號位,也就是第一個PushObject的位置
            ctx.PushReference(0);
            //壓入參數3,val,同ref/out
            ctx.PushReference(1);
            ctx.Invoke();
            //讀取0號位的值
            List<int> lst = ctx.ReadObject<List<int>>(0);
            initialVal = ctx.ReadInteger(1);

            Debug.Log(string.Format("lst[0]={0}, initialVal={1}", lst[0], initialVal));
        }

2  委托

委托適配器(DelegateAdapter)

如果將委托實例傳出給ILRuntime外部使用,那就意味着需要將委托實例轉換成真正的CLR(C#運行時)委托實例,這個過程需要動態創建CLR的委托實例。由於IL2CPP之類的AOT編譯技術無法在運行時生成新的類型,所以在創建委托實例的時候ILRuntime選擇了顯式注冊的方式,以保證問題不被隱藏到上線后才發現。

 

委托轉換器(DelegateConvertor)

ILRuntime內部是使用Action,以及Func這兩個系統自帶委托類型來生成的委托實例,所以如果你需要將一個不是Action或者Func類型的委托實例傳到ILRuntime外部使用的話,除了委托適配器,還需要額外寫一個轉換器,將Action和Func轉換成你真正需要的那個委托類型。

 

//TestDelegateMethod, 這個委托類型為有個參數為int的方法,注冊僅需要注冊不同的參數搭配即可
appdomain.DelegateManager.RegisterMethodDelegate<int>();
//帶返回值的委托的話需要用RegisterFunctionDelegate,返回類型為最后一個
appdomain.DelegateManager.RegisterFunctionDelegate<int, string>();
//Action<string> 的參數為一個string
appdomain.DelegateManager.RegisterMethodDelegate<string>();

//ILRuntime內部是用Action和Func這兩個系統內置的委托類型來創建實例的,所以其他的委托類型都需要寫轉換器
//將Action或者Func轉換成目標委托類型

appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateMethod>((action) =>
{
//轉換器的目的是把Action或者Func轉換成正確的類型,這里則是把Action<int>轉換成TestDelegateMethod
return new TestDelegateMethod((a) =>
{
//調用委托實例
((System.Action<int>)action)(a);
});
});
//對於TestDelegateFunction同理,只是是將Func<int, string>轉換成TestDelegateFunction
appdomain.DelegateManager.RegisterDelegateConvertor<TestDelegateFunction>((action) =>
{
return new TestDelegateFunction((a) =>
{
return ((System.Func<int, string>)action)(a);
});
});

//下面再舉一個這個Demo中沒有用到,但是UGUI經常遇到的一個委托,例如UnityAction<float>
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction<float>>((action) =>
{
return new UnityEngine.Events.UnityAction<float>((a) =>
{
((System.Action<float>)action)(a);
});
});

 

void OnHotFixLoaded()
{
Debug.Log("完全在熱更DLL內部使用的委托,直接可用,不需要做任何處理");

Debug.Log("如果需要跨域調用委托(將熱更DLL里面的委托實例傳到Unity主工程用), 就需要注冊適配器");
Debug.Log("這是因為iOS的IL2CPP模式下,不能動態生成類型,為了避免出現不可預知的問題,我們沒有通過反射的方式創建委托實例,因此需要手動進行一些注冊");
Debug.Log("如果沒有注冊委托適配器,運行時會報錯並提示需要的注冊代碼,直接復制粘貼到ILRuntime初始化的地方");
appdomain.Invoke("HotFix_Project.TestDelegate", "Initialize2", null, null);
appdomain.Invoke("HotFix_Project.TestDelegate", "RunTest2", null, null);
Debug.Log("運行成功,我們可以看見,用Action或者Func當作委托類型的話,可以避免寫轉換器,所以項目中在不必要的情況下盡量只用Action和Func");
Debug.Log("另外應該盡量減少不必要的跨域委托調用,如果委托只在熱更DLL中用,是不需要進行任何注冊的");
Debug.Log("---------");
Debug.Log("我們再來在Unity主工程中調用一下剛剛的委托試試");
TestMethodDelegate(789);
var str = TestFunctionDelegate(098);
Debug.Log("!! OnHotFixLoaded str = " + str);
TestActionDelegate("Hello From Unity Main Project");

}

 

 

 

4 使用熱更新中的接口 創建熱更中的類實例

Unity中 

public abstract class TestClassBase
{
    public virtual int Value
    {
        get
        {
            return 0;
        }
        set
        {

        }
    }

    public virtual void TestVirtual(string str)
    {
        Debug.Log("!! TestClassBase.TestVirtual, str = " + str);
    }

    public abstract void TestAbstract(int gg);
}

Hotfix

   public class TestInheritance : TestClassBase
    {
        public override int Value { get; set; }
        public override void TestAbstract(int gg)
        {
            UnityEngine.Debug.Log("!! TestInheritance.TestAbstract gg =" + gg);
        }

        public override void TestVirtual(string str)
        {
            base.TestVirtual(str);
            UnityEngine.Debug.Log("!! TestInheritance.TestVirtual str =" + str);
        }

        public static TestInheritance NewObject()
        {
            return new HotFix_Project.TestInheritance();
        }
    }

實際Unity工程使用時要通過適配器注冊來創建實例

  Debug.Log("首先我們來創建熱更里的類實例");
        TestClassBase obj;
        Debug.Log("現在我們來注冊適配器, 該適配器由ILRuntime/Generate Cross Binding Adapter菜單命令自動生成");
        appdomain.RegisterCrossBindingAdaptor(new TestClassBaseAdapter());
        Debug.Log("現在再來嘗試創建一個實例");
        obj = appdomain.Instantiate<TestClassBase>("HotFix_Project.TestInheritance");
        Debug.Log("現在來調用成員方法");
        obj.TestAbstract(123);
        obj.TestVirtual("Hello");
        obj.Value = 233;
        Debug.LogFormat("obj.Value={0}", obj.Value);


        Debug.Log("現在換個方式創建實例");
        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);

5 CLR 重定向

...略

6 CLR綁定

通常情況下,如果要從熱更DLL中調用Unity主工程或者Unity的接口,是需要通過反射接口來調用的,包括市面上不少其他熱更方案,也是通過這種方式來對CLR方接口進行調用的。

但是這種方式有着明顯的弊端,最突出的一點就是通過反射來調用接口調用效率會比直接調用低很多,再加上反射傳遞函數參數時需要使用object[]數組,這樣不可避免的每次調用都會產生不少GC Alloc。眾所周知GC Alloc高意味着在Unity中執行會存在較大的性能問題。

ILRuntime通過CLR方法綁定機制,可以選擇性的對經常使用的CLR接口進行直接調用,從而盡可能的消除反射調用開銷以及額外的GC Alloc

 

7 協程調用 同樣需要注冊適配器

 //這里做一些ILRuntime的注冊
        //使用Couroutine時,C#編譯器會自動生成一個實現了IEnumerator,IEnumerator<object>,IDisposable接口的類,因為這是跨域繼承,所以需要寫CrossBindAdapter(詳細請看04_Inheritance教程),Demo已經直接寫好,直接注冊即可
        appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
unsafe void OnHotFixLoaded()
{
  appdomain.Invoke("HotFix_Project.TestCoroutine", "RunTest", null, null);
}

public void DoCoroutine(IEnumerator coroutine)
{
  StartCoroutine(coroutine);
}

 

8 在熱更中使用 MonoBehaviour

。。。略

 

9 Unity工程中反射熱更中的類和屬性

Debug.Log("C#工程中反射是一個非常經常用到功能,ILRuntime也對反射進行了支持,在熱更DLL中使用反射跟原生C#沒有任何區別,故不做介紹");
        Debug.Log("這個Demo主要是介紹如何在主工程中反射熱更DLL中的類型");
        Debug.Log("假設我們要通過反射創建HotFix_Project.InstanceClass的實例");
        Debug.Log("顯然我們通過Activator或者Type.GetType(\"HotFix_Project.InstanceClass\")是無法取到類型信息的");
        Debug.Log("熱更DLL中的類型我們均需要通過AppDomain取得");
        var it = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
        Debug.Log("LoadedTypes返回的是IType類型,但是我們需要獲得對應的System.Type才能繼續使用反射接口");
        var type = it.ReflectionType;
        Debug.Log("取得Type之后就可以按照我們熟悉的方式來反射調用了");
        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));

 

10  Unity值類型綁定

注冊值類型綁定 用來提高效率

 //這里做一些ILRuntime的注冊,這里我們注冊值類型Binder,注釋和解注下面的代碼來對比性能差別
        appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
        appdomain.RegisterValueTypeBinder(typeof(Quaternion), new QuaternionBinder());
        appdomain.RegisterValueTypeBinder(typeof(Vector2), new Vector2Binder());

 

Debug.Log("Vector3等Unity常用值類型如果不做任何處理,在ILRuntime中使用會產生較多額外的CPU開銷和GC Alloc");
Debug.Log("我們通過值類型綁定可以解決這個問題,只有Unity主工程的值類型才需要此處理,熱更DLL內定義的值類型不需要任何處理");
Debug.Log("請注釋或者解注InitializeILRuntime里的代碼來對比進行值類型綁定前后的性能差別");

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM