原文:http://www.sohu.com/a/123334175_355140
作者|車雄生
編輯|木環
騰訊最近在開源方面的動作不斷:先是微信跨平台基礎組件Mars宣布開源,騰訊手游又於近期開源了Unity3D下Lua編程解決方案——xLua。xLua,何方神聖?有哪些技術細節可以說道說道?
寫在前面
xLua是Unity3D下Lua編程解決方案,自2016年初推廣以來,已經應用於十多款騰訊自研游戲,因其良好性能、易用性、擴展性而廣受好評。現在 騰訊已經將xLua開源到GitHub。
2016年12月末,xLua剛剛實現新的突破:全平台支持用Lua修復C#代碼bug。目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現,不足之處在於:
-
接入成本高,有的項目已經用C#寫完了,這時要接入需要把需要熱更的地方用Lua重新實現;
-
即使一開始就接入了,也存在同時用兩種語言開發難度較大的問題;
-
Lua性能不如C#;
xLua熱補丁技術支持在運行時把一個C#實現(函數,操作符,屬性,事件,或者整個類)替換成Lua實現,意味着你可以:
-
平時用C#開發;
-
運行也是C#,性能秒殺Lua;
-
有bug的地方下發個Lua腳本fix了,下次整體更新時可以把Lua的實現換回正確的C#實現,更新時甚至可以做到不重啟游戲;
這個新特性iOS,Android,Windows,Mac都測試通過了,目前在做一些易用性優化。那么,騰訊開源的xLua究竟是怎樣的技術?它是為何如此設計的?更令人關心的是,xLua的性能如何?帶着這些問題,InfoQ對其作者進行了采訪並將內容整理成文。
技術背景
騰訊自研手游,就我了解的項目來說,大多數游戲引擎都是Unity3D,少數用coco2d。
xLua這個插件具體用到了哪些游戲中?雖說xLua是2015年3月就完成了第一個版本,但由於當時項目組熱更的意識並沒有很普遍,需求不是很強烈,xLua的開發資源都調到更緊急的項目了。直到15年年底正式集成到我們的apollo手游開發框架,才迎來xLua的第一個項目。到目前為止,我們已知的應用了xLua的項目有十多個,其中不乏一些重量級IP,或者按星級標准打造的產品。
在xLua之前,面對iOS無法熱更新的問題,有用ulua的,有用slua的,也有項目用自研的腳本語言,不過當時用人更新的項目也不多。
熱更新流程
手游的熱更新流程很簡單,只是啟動時檢測下是否有新版本文件,有的話就下載覆蓋老文件,然后啟動。
下載的文件如果是圖片,模型這些是沒問題的,但如果是Unity原生的代碼邏輯,無論是以前的Mono AOT或者后來的il2cpp,都是編譯成native code,iOS下是跑不了的。解決辦法就一個,別用native code,別用jit,解析執行就可以了。包括xLua在內的所有熱更新支持方案都是通過“解析執行”來實現代碼邏輯熱更新。
來自xLua的 Hello world
三行代碼跑lua腳本
一個完整的例子僅需3行代碼:
下載xLua后解壓到Unity工程Assets目錄下,建一個MonoBehaviour拖到場景,在Start里頭加上這么三行:
XLua.LuaEnv luaenv = new XLua.LuaEnv();luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");luaenv.Dispose();
運行就可以看到Console打印的hello world。
-
第一和第三行分別LuaEnv的創建以及銷毀,所謂LuaEnv可以理解為lua虛擬機,往往整個工程一個虛擬機即可:
-
DoString里頭可以是任意合法的lua代碼,例子中調用了UnityEngine.Debug.Log接口打印了一個log(C#的靜態函數在CS下直接可用);
C#調用lua系統函數math.max
xLua支持把一個Lua函數綁定到C# delegate。
我們先聲明一個delegate,並為它加上CSharpCallLua標簽:
[XLua.CSharpCallLua]public delegate double LuaMax(double a, double b);
然后在上面那例子加上這么兩行(luaenv銷毀前):
var max = luaenv.Global.GetInPath<LuaMax>("math.max");Debug.Log("max:" + max(32, 12));
就那么簡單,把lua的math.max綁定到C#的max變量后,調用就和一個C#函數調用差不多了,而且,最最重要的是,執行了“XLua/Generate Code”后,max(32, 12)調用是不產生(C#)gc alloc的,既優雅,又高效!(更詳細的可以看XLuaDoc下的文檔。)
xLua全局觀
易用性:編輯器下無需生成代碼支持所有特性
xLua的易用不僅僅體現在編程,還體現在方方面面的細節考慮,甚至考慮到團隊配合工作流。xLua僅有兩個菜單選擇,分別是生成代碼和清除生成代碼。在菜單之外,甚至只需要在build手機版本前執行一下“Generate Code”即可(這也有API可集成到項目的自動化打包流程)。
這就是xLua的特色功能之一:編輯器下無需生成代碼支持所有特性。之所以做這個功能,是因為有的項目反饋,“生成代碼”對於策划美術太過遙遠,教了很久還是老忘;還有個大項目反饋說由於代碼很多,每次生成代碼后,Unity3D都要轉很久。
擴展性:授之以魚,不如授之以漁
開發中我們往往要用到很多東西,比如用PB和后台交互,解析json格式的配置文件等等。雖說我們都可以在C#那找到相應的庫,然后通過xLua去使用這些庫,但這效率不高,最好能有相應Lua的庫。
不少方案是直接集成一些常用的Lua庫,但這帶來些新問題:這些庫不一定用到,卻增大安裝包;集成的庫也不一定符合項目習慣:json解析有人喜歡rapidjson,有人愛用cjson,所謂眾口難調;對於某些項目,這些庫還是不夠,還是得自己去想辦法加;
騰訊團隊的設計原則是授之以魚,不如授之以漁,因此xLua:
-
提供了接口、教程,在不修改xLua代碼的情況下,開發者可以根據個人喜好加入庫;
-
通過cmake實現跨平台編譯,可以選擇伴隨xLua一起編譯,修改一個makefile文件,搞定各平台編譯。
-
除了很方便加入第三方Lua插件,xLua的生成引擎支持二次開發,可以編寫生成插件,生成自己所需的一些代碼以及配置。
性能的保證
游戲的性能備受關注,因此任何模塊的變化都需要盡可能不降低甚至調優游戲整體的性能。xLua設計原則是在保證運行效率的前提下,盡量的保證開發效率。
對於性能這塊,有幾個至關重要的版本:
第一個版本1.0.0在05年3月份發布,當時delegate,interface作為最主要的C#訪問Lua的設定,從接口層面避免了boxing、unboxing、gc alloc,這是一個良好的起點。做一個通用組件的都知道,接口一開始設計不合理導致的問題很難解決,別人已經用了,甚至已經養成習慣了,很難糾正。
ps:說起這習慣,有的從別的lua插件轉為使用xLua的童鞋,一開始習慣用LuaFunction.Call去調用lua(xLua也保留了這接口,可用於性能要求不高的場合),他們后期就痛苦了,還得一個個地方的改回來。
第二個很重要的版本是2.0.0(06年3月發布),這版本主要目標就性能優化,因為當時有個對性能要求極其嚴苛的項目想用lua,嚴苛到什么程度呢?他們覺得C#性能都不放心,戰斗系統打算用C++寫。那版本我們把虛擬機切換到luajit,加入了lazyload技術,逐行語句的優化,甚至關鍵地方不用C#提供的容器,自己寫專用的(比Dictionary實測性能高4倍)。。。可以認為我們重做了一個xLua。最終他們的選型測試結論是選xLua。
后來和一些項目的交流發現,項目組很關注gc alloc這指標,甚至比lua和C#間的互調性能指標還要看重。於是有了2.1.0版本(06年7月發布),這版本主要目標是gc優化,我們重寫了反射,反射調用的gc減少到原來的幾分之一,性能提高了3倍左右。我們設計了一個全新的復雜值類型支持方案,該方案支持的類型更多(只要struct的字段都是值類型即可),包括用戶自定義的struct(別的方案都不支持),也更省內存(Vector3為例,內存占用只有別的方案的30%)。
但也有劣勢的地方,比如你調用Vector3上的一些方法,會比ulua、slua要差,因為后面兩個把Vector3用lua重新實現了,這類耗時不大的運算相比lua和C#直接的適配成本小太多了,直接在lua做更划算,不過這差距僅限於那幾個ulua、slua完全重新實現的類。
上面只是三個重大節點,我們覺得性能是一個需要持續關注的點:平時想到一個好點子,就會改改,測試下,有提升就加入;建立性能基線,防止某個新功能的加入,某個bug的修改把性能給改壞了。
xLua內置Lua代碼profiler;支持真機調試。目前lua profiler只是一個小工具,所以沒有做圖形化界面,典型的一個報告如下:
網上也有類似的工具,我們這個的優勢是對C#函數的支持以及luajit下更為准確。
真機調試支持各lua插件都一樣,就是把ZeroBraneStudio調試需要用到的luasocket庫預先編譯進去而已,沒什么值得介紹的地方。
技術實現的細節
泛型
泛型類型除了運行時動態實例化之外都支持,而運行時動態實例化需要jit的支持,iOS下行不通。舉個例子,如果你配了對Dictionary 生成代碼,那這個類型是可以用的,但如果你新更新的lua代碼,想用一個Dictionary ,這個類型之前沒生成代碼,而且C#里頭也沒任何地方使用過,這就不支持。靜態實例化的泛型,其實和非泛型類型處理上沒區別。
委托事件的封裝
委托封裝是根據委托的接口生成一段操作lua棧的代碼作為委托的實現。舉個例子就很好懂了。比如對於委托:delegate double Add(double a, double b),我們生成如下代碼:
public double SystemDouble(double a, double b){ RealStatePtr L = luaEnv.L; int err_func =LuaAPI.load_error_func(L, errorFuncRef); LuaAPI.lua_getref(L, luaReference); LuaAPI.lua_pushnumber(L, a); LuaAPI.lua_pushnumber(L, b); int __gen_error = LuaAPI.lua_pcall(L, 2, 1, err_func);if (__gen_error != 0) luaEnv.ThrowExceptionFromError(err_func - 1); double __gen_ret = LuaAPI.lua_tonumber(L, err_func + 1); LuaAPI.lua_settop(L, err_func - 1); return __gen_ret;}
這代碼把調用轉給lua函數,調用委托就是調用這函數。
其它方案都有delegate的支持,一般僅用於在lua側主動傳遞/設置一個lua函數到C#,而xLua支持更為完整,比如:
-
支持C#主動用delegate來引用一個lua函數。用delegate代替類似object[] Call(params object[] args)的接口調用lua最大的好處是可以避免值類型傳遞時的boxing/unboxing,還有參數數組,返回值數組的gc alloc;
-
支持返回delegate的delegate,可對應到lua的高階函數;
作為這技術的一個延伸,xLua支持用一個c# interface引用一個lua table,這個特性和一些IOC框架配合可以實現C#和Lua間無感知(模塊間都通過interface耦合,然后由框架去組裝)。
無縫支持生成代碼及反射
生成代碼固然重要,已然是各大主流方案的標配。
反射有的方案明確不支持,但從項目的反饋來說,也是至關重要的:有的項目代碼很多,已經接近蘋果的80M Text段的限制,對他們來說,代碼量大小關乎到能否發布,反射方式性能不如生成代碼,但對安裝包影響小。
這的無縫有兩個含義:
-
兩者在支持的特性以及特性的使用方式都是一致的,兩者方式間切換,業務邏輯代碼不用修改,改改配置就可以了;
-
兩者無縫配合,比如一個繼承鏈上,任意一個類都可以選擇生成代碼或者反射,比如子類選擇生成代碼,父類由於不常用選擇了反射,還是可以在子類對象上調用父類的方法;
對於il2cpp的stripping,xLua也考慮到了,只要你對一個類配置了ReflectionUse,會自動生成Unity的link.xml配置文件,將該類型列為不剪裁。
其他Lua插件一覽
在xLua之外,還有其他的Lua插件,如 uLua、SLua、C#light等。
(1) ulua應用項目是最多的,由於開源得早,名氣也最大,這是它很大的優勢。騰訊也有項目用ulua,反饋比較多的問題是它版本的前后兼容問題:
-
ulua最早是一個叫LuaInterface開源庫的Unity移植,在2015年初換成cs2lua,又在2016年初換成tolua c#,只所以說“換”,是因為這從API角度看可認為三個不同的產品,它們間很難升級,而且是每換一次,之前的版本就徹底不維護了,這給項目帶來很大的困擾。
-
ulua的第一個版本純反射,並不實用,已經淡出市場,現存應用用后兩個版本居多。cstolua版本接口比較混亂:它保留了第一版ulua接口之余,搞了一套新接口,這兩套接口之間並不正交,也不是后者完全替代前者,讓人有點無所適從。到了tolua c#版本,這問題解決了,但同時也把反射特性(老接口)給廢了。不過總體來說,ulua在向好的方向走。
(2) slua代碼質量比cstolua好很多(很多人當時選slua的理由),部分支持反射。性能按我們的測試用例整體比tolua c#略低,另外代碼質量對比tolua c#已經形成不了明顯優勢。
(3) C#light,個人覺得主要有兩個不足:
-
按其實現原理來說,性能不會靠譜,到不了手機上實用的地步;
-
由於不完整支持C#,本質上只是另一種叫C#light的語言(C# like?名字倒很貼切),這兩者代碼配合起來也復雜,甚至它能做到比C#和lua配合更復雜些
事實也證明了,C# light基本淡出市場,可以忽略不計了。
(4) LSharp是C# light作者的后續作品,倒是可以期盼些,從il層面執行,這兩個問題有望改善,可惜后面沒了下文(不維護了)。
相比之下,騰訊在設計xLua時,實現的功能更全,這“全”體現在C#的特性支持得更全些,lua虛擬機版本支持更全;更易用些,比如編輯器下不用生成代碼;另外,性能也不比它們差。
說到功能更全,可能有人抱怨並沒有pb,json,sqlite等等功能。其實稍熟悉lua的人都知道,那只是把一些現成lua擴展編譯進去而已,算不上是它做了這些功能。預集成好處是方便,壞處是沒選擇的余地,用不上的東西會占空間,用得上的東西也不一定是你喜歡的庫。
xLua的lua庫基於cmake編譯,要加這些庫門檻很低,有教程,改一個Makefile搞定各平台編譯。在C#測也提供了api來初始化這些庫。總而言之,xLua的原則是授之以漁。
xLua的靈感來源
xLua立項當初,考察了當時能找到的所有方案,並分析各方案優劣,定出第一個版本的特性,大體是基於NLua基礎上加上代碼生成。介紹下NLua,NLua的作者就是LuaInterface的作者,NLua可以認為是LuaInterface的升級版,而前面也說了,第一版uLua是LuaInterface的Unity移植版本,也不能算原創。
因為是“站在”生成代碼當時有看過cstolua的實現(那時還沒掛ulua的牌),覺得它通過硬編碼字符串拼接的方式維護性不太好,就用模版來做。感覺這步是走對了,后續生成代碼調整起來比較簡單,這對性能調優很有好處。
經過十多個版本的迭代,優化,現在NLua的影子比較淡了(NLua僅支持反射,而xLua的反射在2.1.0版本已經完全重寫),就剩下C#引用類型對象在lua的表達的思路沒變。
此外,遇到需要調整較大的bug,我們也會先看同類插件是不是已經解決了,對比他們的修改方案和我們的,選更適合的。
xLua背后的研發與團隊
xLua目前迭代了十多個版本,從第一個項目開始,平均一個月一個版本。研發團隊人員目前有一個全職開發。測試使用的是騰訊互娛的公有資源,很規范:有一套不斷補充的功能自動化用例,性能測試也建立了基線,確保不會因為功能迭代而影響性能。騰訊互娛有專門的客戶端兼容性測試實驗室,至少中版本號以上的變動我們會提交給他們針對top 100的機型進行兼容性測試。
至於lua,luajit的更新跟進,先說luajit吧,luajit變動不大,我第一次用luajit是11年,那時支持到lua5.1,現在也還是lua5.1,中間只是一些bug的修復,性能優化,或者新平台支持等,我們要做事情不多。而lua中版本間差別還是蠻大的,但中版本變動並不頻繁,從5.1到5.2用了6年,從5.2到5.3用了3年,5.3是2015年初發布的,我個人覺得到下一次中版本變動會很久,不亞於甚至大於5.1到5.2的時間跨度(5.2個人認為只是一個過渡版本)。
小版本一般改改bug,等穩定后直接升級就可以了,不需要做很多事情,目前xLua的lua版本用的是lua的最新版本5.3.3。
聊聊C#,談談Lua
C#在開發效率和運行效率平衡得很好,語言特性也比較全,個人覺得是很優秀的一門語言。在Unity3D上的缺憾主要是其mono版本太低,一些很古老的bug,比如著名的foreach性能問題很多個版本都沒解決,新的特性,比如await又不支持。
另外在手機平台iOS不允許應用下載native code運行,jit,剛好把mono應用的熱更新給堵死了,要是mono虛擬機能夠做到像luajit那樣,jit走不通就用interpret模式,其實就沒lua或者其它熱更新方案什么事了。
而lua被稱為游戲腳本之王,在游戲領域應用比較廣泛,它設計之初就考慮到嵌入式領域,比如相對它提供的特性來說,它體積非常小,啟動一個vm占資源也不多,性能也是腳本里頭的佼佼者。
lua相對C#而言,首先是它支持解析執行,進而支持熱更新。而免編譯對開發效率提升也是蠻大的,特別是較大的項目。
lua的動態類型有利有弊,好的是沒有編譯期的類型檢查,快速開發比較有優勢,特別在需求三天兩頭就變的游戲領域。缺點是要做出健壯的軟件得有大量的測試來保證,還有由於要做運行期檢查,性能會比靜態類型語言低。
lua的一大特色是語言級的協程(coroutine)的支持,比Unity3D基於generator模擬的協程要好很多,對於復雜異步業務邏輯編寫很有幫助,xLua的配套例子有范例(ps一下,Unity3D的mono版本升級到支持await的話,是更理想的異步方案)。
至於C#和lua間如何配合,可能每個人都有不同的看法,但至少有一點是確定的:需求變更大,預計很可能需要熱更的地方,用lua。當然,也可以嘗試最新的開發模式,全C#開發,lua fix bug。
寫在最后
xLua應該還有不足,我們會在發現的第一時間去修改。騰訊xLua團隊極度歡迎大家在發現不足之后提出反饋。