unity探索者之ILRuntime代碼熱更新


版權聲明:本文為原創文章,轉載請聲明https://www.cnblogs.com/unityExplorer/p/13540784.html

最近幾年,隨着游戲研發質量越來越高,游戲包體大小也是增大不少,熱更新功能就越發顯的重要。

兩、三年前曾用過xlua作為熱更方式,xlua的熱補丁方式對於bug修復、修改類和函數之類的熱更還是比較好用的

但是lua對於中小型團隊並不是那么友好,畢竟會lua的人始終只有一部分,更多的unity開發者還是對c#更熟悉一些

原本c#是有動態編譯功能的,也就是支持熱更新,奈何ios系統不支持jit,禁止mono的動態編譯,並且雖然android支持動態編譯,但實際使用dll熱更的時候坑也不少

於是在ILRuntime的正式版1.0出來后,立馬就去體驗了一下,果然用起來還不錯

截止到目前,ILRuntime的版本已經更新到1.6.4,從1.6開始,ILRuntime也發布到了unity的Package Manager,集成也比之前更方便

如果你使用的是unity2018或更高的版本,那可以直接在Package Manager中找到ILRuntime的包,或者按照ILRuntime的官網說明來集成

如果你使用的是unity2017或更低的版本,官網里也有官方SDK的下載地址

這是ILRuntime的官網:https://ourpalm.github.io/ILRuntime/public/v1/guide/index.html

因為ILRuntime使用unsafe代碼,所以在導入SDK后還需要在設置中允許unsafe代碼,位置在Player Settings -> Other Setttings

說了這么多,該說點干貨了,我先說說怎么使用和加載熱更新文件吧

很多博客中在講ILRuntime熱更新文件的加載時候,都是直接使用WWW下載/加載熱更dll文件,包括ILRuntime的官網中給的示例也是這樣

然而在unity2017乃至更高的版本中,WWW已經被UnityWebRequest取代,並且WWW異步加載本地文件的速度是很慢的,當然這是小問題

重點是dll文件,dll文件的問題在於安全性並不高,有太多的的反編譯工具可以將dll文件反編譯出來

雖然你可以對dll進行加密或者混淆,但是這又會帶來更多新的問題

所以最終我選擇將熱更項目生成的dll文件打成bundle,然后通過AssetBundle.LoadAsset<TextAsset>()讀取。

public static AppDomain appdomain;

static AssetBundle hotfixAB;

/// <summary> /// 加載熱更補丁 /// </summary>

public static void LoadHotFix()

{

  if (hotfixAB)

    hotfixAB.Unload(true);

  hotfixAB = AssetBundle.LoadFromFile("你的熱更bundle文件地址");

  if (hotfixAB)

  {

    appdomain = new AppDomain();

    //加載熱更主體,也就是dll文件

    TextAsset taHotFix = hotfixAB.LoadAsset<TextAsset>("hotfix");

    if (!taHotFix) return;

    using (MemoryStream ms = new MemoryStream(taHotFix.bytes))

    {

      //加載pdb文件,測試用,正式版只需要加載熱更主體

      TextAsset taHotFixPdb = hotfixAB.LoadAsset<TextAsset>("hotfixpdb");

      if (!taHotFixPdb)

        return;

      using (MemoryStream msp = new MemoryStream(taHotFixPdb.bytes))

      {

        //加載熱更的核心函數,如果是正式版,則只傳主體就可以:appdomain.LoadAssembly(ms);

        appdomain.LoadAssembly(ms, msp, new PdbReaderProvider());

      }

    }

  }

}

這種方式實際上是以字節流的形式加載熱更代碼,而bundle實際上也可以通過LoadFromMemory以字節流的形式加載bundle文件,這就意味着你可以任意使用各種加密方式來保證熱更代碼的安全性(當然資源也可以使用這種方式來進行加密)

如何加密bundle這里就不多說了,很多博主都講過,大家可以自行搜索

因為unity組件的特殊性,加載完熱更代碼后,還需要解決跨域繼承和Component的重定向問題

這兩個問題在ILRuntime的官網都有說明,這里就不多說,直接上代碼了

static void InitializeILRuntime()
{
    SetupCLRRedirectionAddComponent();//設置AddComponent的重定向
    SetupCLRRedirectionGetComponent();//設置GetComponent的重定向
    appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());//綁定Coroutine適配器
    appdomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());//綁定MonoBehaviour適配器

    JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);//注冊LitJson的重定向
}

unsafe static void SetupCLRRedirectionAddComponent()
{
    var arr = typeof(GameObject).GetMethods();
    foreach (var i in arr)
    {
        if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1)
        {
            appdomain.RegisterCLRMethodRedirection(i, AddComponent);
        }
    }
}

unsafe static void SetupCLRRedirectionGetComponent()
{
    var arr = typeof(GameObject).GetMethods();
    foreach (var i in arr)
    {
        if (i.Name == "GetComponent" && i.GetGenericArguments().Length == 1)
        {
            appdomain.RegisterCLRMethodRedirection(i, GetComponent);
        }
    }
}

unsafe static StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
    //CLR重定向的說明請看相關文檔和教程,這里不多做解釋
    AppDomain __domain = __intp.AppDomain;

    var ptr = __esp - 1;
    //成員方法的第一個參數為this
    GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
    if (instance == null)
        throw new NullReferenceException();
    __intp.Free(ptr);

    var genericArgument = __method.GenericArguments;
    //AddComponent應該有且只有1個泛型參數
    if (genericArgument != null && genericArgument.Length == 1)
    {
        var type = genericArgument[0];
        object res;
        if (type is CLRType)
        {
            //Unity主工程的類不需要任何特殊處理,直接調用Unity接口
            res = instance.AddComponent(type.TypeForCLR);
        }
        else
        {
            //熱更DLL內的類型比較麻煩。首先我們得自己手動創建實例
            var ilInstance = new ILTypeInstance(type as ILType, false);//手動創建實例是因為默認方式會new MonoBehaviour,這在Unity里不允許
                                                                       //接下來創建Adapter實例
            var clrInstance = instance.AddComponent<MonoBehaviourAdapter.Adaptor>();
            //unity創建的實例並沒有熱更DLL里面的實例,所以需要手動賦值
            clrInstance.ILInstance = ilInstance;
            clrInstance.AppDomain = __domain;
            //這個實例默認創建的CLRInstance不是通過AddComponent出來的有效實例,所以得手動替換
            ilInstance.CLRInstance = clrInstance;

            res = clrInstance.ILInstance;//交給ILRuntime的實例應該為ILInstance

            clrInstance.Awake();//因為Unity調用這個方法時還沒准備好所以這里補調一次
            clrInstance.OnEnable();//因為Unity調用這個方法時還沒准備好所以這里補調一次
        }

        return ILIntepreter.PushObject(ptr, __mStack, res);
    }

    return __esp;
}

unsafe static StackObject* GetComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
    //CLR重定向的說明請看相關文檔和教程,這里不多做解釋
    AppDomain __domain = __intp.AppDomain;

    var ptr = __esp - 1;
    //成員方法的第一個參數為this
    GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
    if (instance == null)
        throw new NullReferenceException();
    __intp.Free(ptr);

    var genericArgument = __method.GenericArguments;
    //GetComponent應該有且只有1個泛型參數
    if (genericArgument != null && genericArgument.Length == 1)
    {
        var type = genericArgument[0];
        object res = null;
        if (type is CLRType)
        {
            //Unity主工程的類不需要任何特殊處理,直接調用Unity接口
            res = instance.GetComponent(type.TypeForCLR);
        }
        else
        {
            //因為所有DLL里面的MonoBehaviour實際都是這個Component,所以我們只能全取出來遍歷查找
            var clrInstances = instance.GetComponents<MonoBehaviourAdapter.Adaptor>();
            for (int i = 0; i < clrInstances.Length; i++)
            {
                var clrInstance = clrInstances[i];
                if (clrInstance.ILInstance != null)//ILInstance為null, 表示是無效的MonoBehaviour,要略過
                {
                    if (clrInstance.ILInstance.Type == type)
                    {
                        res = clrInstance.ILInstance;//交給ILRuntime的實例應該為ILInstance
                        break;
                    }
                }
            }
        }

        return ILIntepreter.PushObject(ptr, __mStack, res);
    }

    return __esp;
}
View Code

然后就是注冊委托的適配器和轉換器了,這個就自己看需求來了

加載熱更文件很簡單,接下來要說的就是如何簡單的去執行和注冊熱更代碼

對於執行熱更代碼,ILRuntime封裝出來的的用發很簡單

調用熱更代碼的核心函數就四行

if (appdomain.LoadedTypes[typeFullName] is ILType type)
{
     IMethod im = type.GetMethod(methodName);
     if (im != null)
         appdomain.Invoke(im, instance, p);
}

當然,實際開發中肯定不止這幾行代碼,對於不同情況,我們可能需要做出不同的處理方案

此外,在實際開發中,也許大部分的函數都需要增加這些代碼,所以,最好的辦法就是將熱更的檢測和執行代碼封裝到一個函數中

//因為程序運行過程中,函數可能會被執行很多次,為了效率,我們將所有被檢測過的函數都保存在字典中
private Dictionary<string, IMethod> iMethods = new Dictionary<string, IMethod>();
//returnObject:熱更函數執行成功后的返回值,若無返回值或熱更函數不存在,則為null
protected bool TryInvokeHotFix(out object returnObject, params object[] p) { returnObject = null;
//對於非靜態函數,需要先創建到熱更類的對象
object instanceHotFix = appdomain.Instantiate(typeName); if (instanceHotFix != null) {
     //通過c#反射提供的接口獲取到執行熱更檢測的函數信息 MethodBase method
= new StackFrame(1).GetMethod(); string methodName = method.Name; int paramCount = method.GetParameters().Length;
//這里將函數名和參數數量進行拼接來作為存儲的key
//當然,如果你確實存在函數名和參數數量均相同,但是參數類型不同的函數的熱更需求,你也可以從GetParameters()中獲取到所有參數的類型,自定義key的組合方式
string key = methodName + "_" + paramCount.ToString(); IMethod im; if (iMethods.ContainsKey(key)) im = iMethods[key]; else { im = type.GetMethod(methodName, paramCount); iMethods.Add(key, im); } if (im != null) { returnObject = appdomain.Invoke(im, instanceHotFix, p); return true; } } return false; }

上面是非靜態函數的熱更檢測執行方法,用起來也很簡單,只要在函數內的頭部執行以下代碼就OK

public int Test(int test)
{
    if (TryInvokeHotFix(out object ob, test))
        return (int)ob;
    return test;
}

對於沒有參數的函數,然后將參數部分傳null,避免new object[],減少GC

對於沒有返回值的,去掉返回值部分就好

if (TryInvokeHotFix(out object ob, null))
    return;

上面是非靜態函數的熱更方法,對於靜態函數,結構大體相同,但是函數內部稍微有點區別

protected static bool TryInvokeStaticHotFix(out object returnObject, params object[] p)
{
    returnObject = null;if (!appdomain.LoadedTypes.ContainsKey(typeFullName))
        return false;

    if (appdomain.LoadedTypes[typeFullName] is ILType type)
    {
MethodBase method = new StackFrame(1).GetMethod(); IMethod im
= type.GetMethod(method.Name, method.GetParameters().Length); if (im != null) { returnObject = appdomain.Invoke(im, null, p); return true; } } return false; }

調用方法就不寫了,和非靜態一樣

除了這兩個核心函數外,還有關於初始化及一些容錯處理,這里我就不寫了,完整的代碼和測試樣例在我的Git項目中有,大家可以通過下方的地址下載


免責聲明!

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



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