騰訊開源手遊熱更新方案:Unity3D下的XLua方案介紹

轉自:http://gad.qq.com/article/detail/7182055

寫在前面

  xLua是Unity3DLua編程解決方案,自2016年初推廣以來,已經應用於十多款騰訊自研遊戲,因其良好性能、易用性、擴展性而廣受好評。現在,騰訊已經將xLua開源到GitHub

  2016年12月末,xLua剛剛實現新的突破:全平臺支持用Lua修復C#代碼bug

  目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現,不足之處在於:

1、接入成本高,有的項目已經用C#寫完了,這時要接入需要把需要熱更的地方用Lua重新實現;

2、即使一開始就接入了,也存在同時用兩種語言開發難度較大的問題;

3、Lua性能不如C#

  xLua熱補丁技術支持在運行時把一個C#實現(函數,操作符,屬性,事件,或者整個類)替換成Lua實現,意味着你可以:

1、平時用C#開發;

2、運行也是C#,性能秒殺Lua

3、有bug的地方下發個Lua腳本fix了,下次整體更新時可以把Lua的實現換回正確的C#實現,更新時甚至可以做到不重啓遊戲;

  這個新特性iOSAndroidWindowMac都測試通過了,目前在做一些易用性優化。

  那麼,騰訊開源的xLua究竟是怎樣的技術?它是爲何如此設計的?更令人關心的是,xLua的性能如何?帶着這些問題,InfoQ對其作者進行了採訪並將內容整理成文。


嘉賓簡介

  車雄生,05年畢業,在華爲工作了6年,跟着先後在兩遊戲創業公司待了幾年,15年進入騰訊互娛公共組件中心。目前專注於一些遊戲公共組件的開發。


技術背景

  騰訊自研手遊,就我瞭解的項目來說,大多數遊戲引擎都是Unity3D,少數用coco2d

  xLua這個插件具體用到了哪些遊戲中?雖說xLua20153月就完成了第一個版本,但由於當時項目組熱更的意識並沒有很普遍,需求不是很強烈,xLua的開發資源都調到更緊急的項目了。直到15年年底正式集成到我們的apollo手遊開發框架,才迎來xLua的第一個項目。到目前爲止,我們已知的應用了xLua的項目有十多個,其中不乏一些重量級IP,或者按星級標準打造的產品。

  xLua之前,面對iOS無法熱更新的問題,有用ulua的,有用slua的,也有項目用自研的腳本語言,不過當時用人更新的項目也不多。

 

熱更新流程

  手遊的熱更新流程很簡單,只是啓動時檢測下是否有新版本文件,有的話就下載覆蓋老文件,然後啓動。

騰訊開源手遊熱更新方案:Unity3D下的XLua方案介紹 - IQ007偉哥 - IQ007偉哥的博客

  下載的文件如果是圖片,模型這些是沒問題的,但如果是Unity原生的代碼邏輯,無論是以前的Mono AOT或者後來的il2cpp,都是編譯成native codeiOS下是跑不了的。

  解決辦法就一個,別用native code,別用jit,解析執行就可以了。包括xLua在內的所有熱更新支持方案都是通過解析執行來實現代碼邏輯熱更新。


來自xLua Hello world

1)三行代碼跑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。

1、第一和第三行分別LuaEnv的創建以及銷燬,所謂LuaEnv可以理解爲lua虛擬機,往往整個工程一個虛擬機即可:

2、DoString裏頭可以是任意合法的lua代碼,例子中調用了UnityEngine.Debug.Log接口打印了一個logC#的靜態函數在CS下直接可用);


2C#調用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("math.max");
Debug.Log("max:"+ max(32, 12));

  就那麼簡單,把luamath.max綁定到C#max變量後,調用就和一個C#函數調用差不多了,而且,最最重要的是,執行了“XLua/Generate Code”後,max(32, 12)調用是不產生(C#gc alloc的,既優雅,又高效!(更詳細的可以看XLua\Doc下的文檔。)

 

xLua全局觀

1)易用性:編輯器下無需生成代碼支持所有特性

  xLua的易用不僅僅體現在編程,還體現在方方面面的細節考慮,甚至考慮到團隊配合工作流。

  xLua僅有兩個菜單選擇,分別是生成代碼和清除生成代碼。在菜單之外,甚至只需要在build手機版本前執行一下“Generate Code”即可(這也有API可集成到項目的自動化打包流程)。

  這就是xLua的特色功能之一:編輯器下無需生成代碼支持所有特性。

  之所以做這個功能,是因爲有的項目反饋,生成代碼對於策劃美術太過遙遠,教了很久還是老忘;還有個大項目反饋說由於代碼很多,每次生成代碼後,Unity3D都要轉很久。

 

2)擴展性:授之以魚,不如授之以漁

  開發中我們往往要用到很多東西,比如用PB和後臺交互,解析json格式的配置文件等等。雖說我們都可以在C#那找到相應的庫,然後通過xLua去使用這些庫,但這效率不高,最好能有相應Lua的庫。

  不少方案是直接集成一些常用的Lua庫,但這帶來些新問題:這些庫不一定用到,卻增大安裝包;集成的庫也不一定符合項目習慣:json解析有人喜歡rapidjson,有人愛用cjson,所謂衆口難調;對於某些項目,這些庫還是不夠,還是得自己去想辦法加;

  騰訊團隊的設計原則是授之以魚,不如授之以漁,因此xLua

   提供了接口、教程,在不修改xLua代碼的情況下,開發者可以根據個人喜好加入庫;

  通過cmake實現跨平臺編譯,可以選擇伴隨xLua一起編譯,修改一個makefile文件,搞定各平臺編譯。

  除了很方便加入第三方Lua插件,xLua的生成引擎支持二次開發,可以編寫生成插件,生成自己所需的一些代碼以及配置。 

3)性能的保證

  遊戲的性能備受關注,因此任何模塊的變化都需要儘可能不降低甚至調優遊戲整體的性能。xLua設計原則是在保證運行效率的前提下,儘量的保證開發效率。

  對於性能這塊,有幾個至關重要的版本:

  第一個版本1.0.0053月份發佈,當時delegateinterface作爲最主要的C#訪問Lua的設定,從接口層面避免了boxingunboxinggc alloc,這是一個良好的起點。做一個通用組件的都知道,接口一開始設計不合理導致的問題很難解決,別人已經用了,甚至已經養成習慣了,很難糾正。ps:說起這習慣,有的從別的lua插件轉爲使用xLua的童鞋,一開始習慣用LuaFunction.Call去調用luaxLua也保留了這接口,可用於性能要求不高的場合),他們後期就痛苦了,還得一個個地方的改回來。

  第二個很重要的版本是2.0.0063月發佈),這版本主要目標就性能優化,因爲當時有個對性能要求極其嚴苛的項目想用lua,嚴苛到什麼程度呢?他們覺得C#性能都不放心,戰鬥系統打算用C++寫。那版本我們把虛擬機切換到luajit,加入了lazyload技術,逐行語句的優化,甚至關鍵地方不用C#提供的容器,自己寫專用的(比Dictionary實測性能高4倍)。。。可以認爲我們重做了一個xLua。最終他們的選型測試結論是選xLua

  後來和一些項目的交流發現,項目組很關注gc alloc這指標,甚至比luaC#間的互調性能指標還要看重。於是有了2.1.0版本(067月發佈),這版本主要目標是gc優化,我們重寫了反射,反射調用的gc減少到原來的幾分之一,性能提高了3倍左右。我們設計了一個全新的複雜值類型支持方案,該方案支持的類型更多(只要struct的字段都是值類型即可),包括用戶自定義的struct(別的方案都不支持),也更省內存(Vector3爲例,內存佔用只有別的方案的30%)。但也有劣勢的地方,比如你調用Vector3上的一些方法,會比uluaslua要差,因爲後面兩個把Vector3lua重新實現了,這類耗時不大的運算相比luaC#直接的適配成本小太多了,直接在lua做更划算,不過這差距僅限於那幾個uluaslua完全重新實現的類。

  上面只是三個重大節點,我們覺得性能是一個需要持續關注的點:平時想到一個好點子,就會改改,測試下,有提升就加入;建立性能基線,防止某個新功能的加入,某個bug的修改把性能給改壞了。

  xLua內置Lua代碼profiler;支持真機調試。目前lua profiler只是一個小工具,所以沒有做圖形化界面,典型的一個報告如下:

騰訊開源手遊熱更新方案:Unity3D下的XLua方案介紹 - IQ007偉哥 - IQ007偉哥的博客


  網上也有類似的工具,我們這個的優勢是對C#函數的支持以及luajit下更爲準確。

  真機調試支持各lua插件都一樣,就是把ZeroBraneStudio調試需要用到的luasocket庫預先編譯進去而已,沒什麼值得介紹的地方。

 

技術實現的細節

(1) 泛型

  泛型類型除了運行時動態實例化之外都支持,而運行時動態實例化需要jit的支持,iOS下行不通。舉個例子,如果你配了對Dictionary生成代碼,那這個類型是可以用的,但如果你新更新的lua代碼,想用一個Dictionary,這個類型之前沒生成代碼,而且C#裏頭也沒任何地方使用過,這就不支持。靜態實例化的泛型,其實和非泛型類型處理上沒區別。

(2) 委託事件的封裝

  委託封裝是根據委託的接口生成一段操作lua棧的代碼作爲委託的實現。舉個例子就很好懂了。比如對於委託:delegatedouble 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

  支持返回delegatedelegate,可對應到lua的高階函數;

  作爲這技術的一個延伸,xLua支持用一個c# interface引用一個lua table,這個特性和一些IOC框架配合可以實現C#Lua間無感知(模塊間都通過interface耦合,然後由框架去組裝)。


(3) 無縫支持生成代碼及反射

  生成代碼固然重要,已然是各大主流方案的標配。

  反射有的方案明確不支持,但從項目的反饋來說,也是至關重要的:有的項目代碼很多,已經接近蘋果的80M Text段的限制,對他們來說,代碼量大小關乎到能否發佈,反射方式性能不如生成代碼,但對安裝包影響小。

  這的無縫有兩個含義:

1、兩者在支持的特性以及特性的使用方式都是一致的,兩者方式間切換,業務邏輯代碼不用修改,改改配置就可以了;

2、兩者無縫配合,比如一個繼承鏈上,任意一個類都可以選擇生成代碼或者反射,比如子類選擇生成代碼,父類由於不常用選擇了反射,還是可以在子類對象上調用父類的方法;

對於il2cppstrippingxLua也考慮到了,只要你對一個類配置了ReflectionUse,會自動生成Unitylink.xml配置文件,將該類型列爲不剪裁。


其他Lua插件一覽

  xLua之外,還有其他的Lua插件,如 uLuaSLuaC#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) LSharpC# light作者的後續作品,倒是可以期盼些,從il層面執行,這兩個問題有望改善,可惜後面沒了下文(不維護了)。

  相比之下,騰訊在設計xLua時,實現的功能更全,這體現在C#的特性支持得更全些,lua虛擬機版本支持更全;更易用些,比如編輯器下不用生成代碼;另外,性能也不比它們差。

  說到功能更全,可能有人抱怨並沒有pbjsonsqlite等等功能。其實稍熟悉lua的人都知道,那只是把一些現成lua擴展編譯進去而已,算不上是它做了這些功能。預集成好處是方便,壞處是沒選擇的餘地,用不上的東西會佔空間,用得上的東西也不一定是你喜歡的庫。

  xLua的lua庫基於cmake編譯,要加這些庫門檻很低,有教程,改一個Makefile搞定各平臺編譯。在C#測也提供了api來初始化這些庫。總而言之,xLua的原則是授之以漁。


xLua的靈感來源

  xLua立項當初,考察了當時能找到的所有方案,並分析各方案優劣,定出第一個版本的特性,大體是基於NLua基礎上加上代碼生成。介紹下NLuaNLua的作者就是LuaInterface的作者,NLua可以認爲是LuaInterface的升級版,而前面也說了,第一版uLuaLuaInterfaceUnity移植版本,也不能算原創。

  因爲是站在生成代碼當時有看過cstolua的實現(那時還沒掛ulua的牌),覺得它通過硬編碼字符串拼接的方式維護性不太好,就用模版來做。感覺這步是走對了,後續生成代碼調整起來比較簡單,這對性能調優很有好處。

  經過十多個版本的迭代,優化,現在NLua的影子比較淡了(NLua僅支持反射,而xLua的反射在2.1.0版本已經完全重寫),就剩下C#引用類型對象在lua的表達的思路沒變。

  此外,遇到需要調整較大的bug,我們也會先看同類插件是不是已經解決了,對比他們的修改方案和我們的,選更適合的。


xLua背後的研發與團隊

  xLua目前迭代了十多個版本,從第一個項目開始,平均一個月一個版本。

  研發團隊人員目前有一個全職開發。測試使用的是騰訊互娛的公有資源,很規範:有一套不斷補充的功能自動化用例,性能測試也建立了基線,確保不會因爲功能迭代而影響性能。騰訊互娛有專門的客戶端兼容性測試實驗室,至少中版本號以上的變動我們會提交給他們針對top 100的機型進行兼容性測試。

  至於lualuajit的更新跟進,先說luajit吧,luajit變動不大,我第一次用luajit11年,那時支持到lua5.1,現在也還是lua5.1,中間只是一些bug的修復,性能優化,或者新平臺支持等,我們要做事情不多。而lua中版本間差別還是蠻大的,但中版本變動並不頻繁,從5.15.2用了6年,從5.25.3用了3年,5.32015年初發布的,我個人覺得到下一次中版本變動會很久,不亞於甚至大於5.15.2的時間跨度(5.2個人認爲只是一個過渡版本)。

  小版本一般改改bug,等穩定後直接升級就可以了,不需要做很多事情,目前xLualua版本用的是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一下,Unity3Dmono版本升級到支持await的話,是更理想的異步方案)。

  至於C#lua間如何配合,可能每個人都有不同的看法,但至少有一點是確定的:需求變更大,預計很可能需要熱更的地方,用lua。當然,也可以嘗試最新的開發模式,全C#開發,lua fix bug


寫在最後

  xLua應該還有不足,我們會在發現的第一時間去修改。騰訊xLua團隊極度歡迎大家在發現不足之後提出反饋。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章