對老項目進行熱更新
項目用純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常見問題:Lua腳本熱重載,內存狀態數據丟失?