另類Unity熱更新大法:代碼注入式補丁熱更新


對老項目進行熱更新

項目用純C#開發的?

眼看Unity引擎熱火朝天,無數程序猿加入到了Unity開發的大本營。

一些老項目,在當時ulua/slua還不如今天那樣的成熟,因此他們選擇了全c#開發;也有一些出於性能考慮,全c#開發;也有一些沒有太豐富運營經驗的開發團隊,沒有想太多,用全c#爽爽地開發。

策划和運營要熱更新?

用C#開發爽爽的日子一天一天的過去了,直到突然有一天,策划老大說:“我們得做個熱更新模塊!”;
突然有一天,老板說:“別人游戲用Lua熱更新,為什么我們不行?”;
突然有一天,運營說:“線上游戲出了個bug,重新編譯出包審核得幾天啊!”——嗯,這時候,受傷的總是程序猿。

打補丁方法

有沒有亡羊補牢,臨危受命的折衷方法?可以不用把C#改成Lua,可以不用區分平台(AndroidDLL重載IOS卻不行),可以對任何代碼做修復的方法?

有的,並且用很笨的一句代碼來概括:

class Fucker {
    void Fucking() {
        if (PatchScript.HasPatchScript("Fucker.Fucking")) {
            // do patch fuck
            PatchScript.CallPatchScript("Fucker.Fucking");
            return;
        }
        // do origin fuck
        Log.Info("I am a original fuck");
    }
}

往所有的函數注入代碼,當存在補丁腳本時執行補丁腳本,不存在時執行原代碼。

因此,本文的熱更新等同於打補丁

什么是熱更新?

吐槽一點,雖然我們這個方法確實將熱更新做成模塊了,但這絕對是迫不得已的。 熱更新絕對不是一個功能模塊能實現,它是一個底層架構所決定的。要說一個項目不好,無法實現熱更新,這歸根到底是架構沒想好、策划沒堅持、程序沒執着、運營懶得管等等各種各樣復雜原因所導致的。

我理想的熱更新是怎樣的?

我心目中理想熱更新是怎樣?要熱!

  • 對任意部位的代碼進行修改;
  • 運行時,自動下載更新代碼,爾后無需重啟;
  • 運行時,立即重載代碼,並繼續運行;
  • 兼顧開發環境與生產環境的簡便性;

達到什么目地

熱更新在Web開發領域非常普遍,畢竟HTTP是無狀態的;而游戲這種高實時性的開發相比,要想做好熱更新就確實需要架構層的更多考慮了。怎么做好熱更新,我們還是回到主題,接下來介紹方法,可以達到什么目的:

  • 對任意部位的方法體代碼進行修改;
  • 運行時,立即重載代碼,並繼續運行
  • 語言無關:同樣的思路可以應用在Java、C#、Go、C++等等
  • 使用起來不太方便
  • 亡羊補牢專用

代碼注入補丁的熱更新

上面說了很多廢話。接下直奔主題,要怎樣做到:

注入補丁

class Fucker {
    void Fucking() {
        if (PatchScript.HasPatchScript("Fucker.Fucking")) {
            // do patch fuck
            PatchScript.CallPatchScript("Fucker.Fucking");
            return;
        }
        // do origin fuck
        Log.Info("I am a original fuck");
    }
}

執行Lua腳本

我們要針對Fucker類的Fucking方法進行更新,則新建Lua腳本Fucker.Fucking.lua

-- 文件名Fucker.Fucking.lua

function Func()
    print("I am a patch fuck!")
end

return Func

一個補丁腳本就此完成,當程序運行到Fucking函數時,實際上它執行的是Lua腳本,變相的實現了熱更新的功能——改變代碼的執行行為。

熱更新大法流程

STEP 1:執行環境

本文針對Unity游戲開發,那么原語言,當然是C#了;而打補丁的語言,使用Lua;
在這里我們使用SLua插件,它的高質量代碼和強大的反射功能,非常適合代碼注入補丁熱更新。

class PatchScript
{
    public bool HasPatchScript(string path)
    {
        return File.Exists("Script/" + path + ".lua");
    }

    public void CallScript(string path)
    {
        string scriptCode = File.ReadAllString(path);
        var luaFunc = this.luaState.doScript(scriptCode) as LuaFunction;
        luaFunc.call();
    }
}

STEP 2:代碼注入

嗯,執行環境,非常的簡單,不就是簡單的if判斷嗎? 估計最令人迷惑的部分就是,如何往所有的C#函數體前部分插入代碼了。

我們要做的,遍歷所有的c#文件,取得class類名,然后再分析函數名,定位函數在代碼中的起始位置、獲取函數的參數列表、參數類型……等等。看起來很復雜,是不是要對c#做語法分析、詞法分析了?感覺工作量很大啊。

幸好,輪子已經做好了。這里要用到一個重要的庫——NRefactory。包括IDE MonoDevelop中的語法智能提示、重構都是基於這個庫進行的。有了它,語法分析詞法分析僅僅是API的調用而已。

找出C#方法體並插入代碼

我們要做的,就是使用NRefactory找出C#的方法體,並插入代碼:

            using (var script = new DocumentScript(document, formattingOptions, options))
            {
                CSharpParser parser = new CSharpParser();
                SyntaxTree syntaxTree = parser.Parse(code, srcFilePath);
                foreach (var classDec in syntaxTree.Descendants.OfType<TypeDeclaration>())
                {
                    if (classDec.ClassType == ClassType.Class || classDec.ClassType == ClassType.Struct)
                    {
                        var className = classDec.Name;
                        foreach (var method in classDec.Children.OfType<MethodDeclaration>())
                        {
                            var returnType = method.ReturnType.ToString();
                            if (returnType.Contains("IEnumerator") || returnType.Contains("IEnumerable"))  // 暫不支持yield!
                                continue;

                           // 。。。。這里找到了方法體! 開始進行插入!
                        }
                    }
                }
            }

我把使用NRefactory對C#方法體注入的代碼,抽象成一個單獨的類MethodInjector(C#),看文章底部。

STEP 3:編寫Lua補丁

補丁的方法,在上文“代碼注入補丁熱更新大法流程”中已有提及:

對需要打補丁的函數,創建Lua腳本

如上文中要改變Fucker.Fucking函數的執行行為,則創建Fucker.Fucking.lua腳本文件,腳本末端返回一個Lua函數。

最后

本文着重提供了一種思路,而不提供完整的源代碼,畢竟涉及到部分人的商業利益,遺憾點到即止。
使用下面的MethodInjector類,會把函數的參數值也進行解析、預編譯指令引入、並且可以在Lua補丁中控制是否在執行補丁后,繼續執行原C#代碼,基本能達到大部分的需求了。

這里舉例一個更好的方案:注入DLL的IL代碼,而不是注入c#代碼,來確保我們的c#代碼不會被改動。

MethodInjector類

完整代碼:
https://github.com/zhaoqingqing/blog_samplecode/blob/master/unity-framework/MethodInjector.cs

版權說明

文/公的Kelly[mr-kelly](簡書作者)     Email: 23110388@qq.com
原文鏈接:http://www.jianshu.com/p/481994e8b7df
著作權歸作者所有,轉載請聯系作者獲得授權,,並標注“簡書作者”。

KSFramework系列

github地址:https://github.com/mr-kelly/KSFramework

歡迎大家到 github提issues


KSFramework:集成U3D熱重載框架 - README

KSFramework:Unity3D開發框架快速入門

KEngine策划指南:配置表格的編輯與編譯

KEngine:Unity3D資源的打包、加載、調試監控

KSFramework常見問題:Lua腳本熱重載,內存狀態數據丟失?

KSFramework常見問題:Excel如何進行SVN協作、差異比較?

KSFramework配置表:擴展表格解析類型

另類Unity熱更新大法:代碼注入式補丁熱更新


免責聲明!

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



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