1. 程式人生 > 實用技巧 >unity探索者之ILRuntime程式碼熱更新

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專案中有,大家可以通過下方的地址下載