Unity3D手游项目的总结和思考(6) - Xlua的使用心得
? ? ? 有一個項目做完快上線了,不是lua寫的,能熱更新的東西就特別少,如果遇到bug也很難在第一時間熱修復,所以我就接入了Xlua這個插件點擊打開鏈接
? ? ? 原本只是想熱修復一下的,后來領導要求把邏輯系統的C#代碼全部換成了Lua,至于為什么,因為他們習慣了每天都更新和修改的開發模式...所以我們干了一件極其喪心病狂的事情,就是邏輯系統的C#代碼全部翻譯成了lua代碼,全手動翻譯...我保證,打死以后也不會再干類似的事情...
? ? ?Xlua特別好用,但是在使用過程中,我發現其實并不是那么簡單的,有很多值得注意的地方.
1.接入Xlua
? ? ? ?接入的門檻,說低呢,也不低,因為官方編譯的版本,很少集成第三方庫,如果你要用proto buffer這種序列化庫,就得自己集成自己編譯,據我了解,大部分的人都得自己編譯,因為proto buffer庫的原因.說門檻高呢,也不高,因為作者寫了一堆自動編譯的腳本,你只需要點擊運行.但是有兩個值得注意的地方.一是編譯工具的版本,盡量用作者指定的,不然出了問題夠你折騰,還有就是編譯的平臺.Windows的庫在Windows下面編譯,ios的庫在mac編譯,而安卓的庫,可以在linux,也可以在mac下面,我建議在mac編譯安卓的庫.
2.LuaBehaviour
? ? ?LuaBehaviour是lua和Unity的交互腳本,在lua中也可以像MonoBehaviour腳本一樣使用.LuaBehaviour,官方提供了一個例子,但只是告訴你一個實現思路,真要在項目中用起來,有些地方還得改進才行.
官方例子:
using UnityEngine; using System.Collections; using System.Collections.Generic; using XLua; using System;[System.Serializable] public class Injection {public string name;public GameObject value; }[LuaCallCSharp] public class LuaBehaviour : MonoBehaviour {public TextAsset luaScript;public Injection[] injections;internal static LuaEnv luaEnv = new LuaEnv(); //all lua behaviour shared one luaenv only!internal static float lastGCTime = 0;internal const float GCInterval = 1;//1 second private Action luaStart;private Action luaUpdate;private Action luaOnDestroy;private LuaTable scriptEnv;void Awake(){scriptEnv = luaEnv.NewTable();LuaTable meta = luaEnv.NewTable();meta.Set("__index", luaEnv.Global);scriptEnv.SetMetaTable(meta);meta.Dispose();scriptEnv.Set("self12", this);foreach (var injection in injections){scriptEnv.Set(injection.name, injection.value);}scriptEnv.Set("transform", transform);luaEnv.DoString(luaScript.text, "LuaBehaviour", scriptEnv);Action luaAwake = scriptEnv.Get<Action>("awake");scriptEnv.Get("start", out luaStart);scriptEnv.Get("update", out luaUpdate);scriptEnv.Get("ondestroy", out luaOnDestroy);if (luaAwake != null){luaAwake();}}// Use this for initializationvoid Start (){if (luaStart != null){luaStart();}}// Update is called once per framevoid Update (){if (luaUpdate != null){luaUpdate();}if (Time.time - LuaBehaviour.lastGCTime > GCInterval){luaEnv.Tick();LuaBehaviour.lastGCTime = Time.time;}}void OnDestroy(){if (luaOnDestroy != null){luaOnDestroy();}luaOnDestroy = null;luaUpdate = null;luaStart = null;scriptEnv.Dispose();injections = null;} }一.lua腳本用TextAsset來保存是不行的,因為這種的話,就會把lua文件打包進prefab里面.lua和prefab需要解耦,那么保存一個lua文件名字是更好的辦法.用到的時候,再根據名字加載.
二.動態掛接這個腳本的問題,在prefab上靜態掛接這個腳本沒有這個問題,但是如果要在代碼中動態掛接這個腳本就有問題,Awake初始化的時候,并沒有設置lua腳本的名字,無法加載lua文件.解決辦法有兩種,一種是先隱藏掛腳本的游戲對象,掛上去后,設置好lua腳本名字再激活,這樣的壞處是,隱藏和激活可能會影響腳本邏輯.另外一種完美的辦法是,掛腳本后,自動調用的Awake和OnEnable跳過,設置好lua名字后,再手動調用
public void Awake(){// 動態掛接LuaBehaviour,Awake調用的時候luaScriptName還未設置,是null,直接return,我們后續手動調用Awakeif (string.IsNullOrEmpty(luaScriptName))return; public void OnEnable(){// 動態掛接LuaBehaviour,第一次OnEnable調用的時候luaScriptName還未設置,是null,直接return,我們后續手動調用第一次的OnEnableif (string.IsNullOrEmpty(luaScriptName))return;lua代碼封裝的手動掛接腳本的函數:
function AddLuaBehaviour(go, luaScriptName, dontDestroyOnLoad) local behaviour = go:AddComponent(typeof(CS.LuaBehaviour)) behaviour.luaScriptName = luaScriptName behaviour.dontDestroyOnLoad = dontDestroyOnLoad if go.activeSelf and go.activeInHierarchy then behaviour:Awake() behaviour:OnEnable() end return behaviour end三.重復初始化LuaBehaviour的性能問題
? ? ? ? 如果你給10個怪物掛上一個LuaBehaviour,關聯的都是同樣一個monster.lua的腳本,那么這10個怪物每次初始化的DoString都會編譯monster.lua...這會帶來沒必要的性能開銷,其實只需要編譯一次.如果只編譯一次呢,用LoadString來替代,緩存LoadString返回的LuaFunction,下次重復使用,使用的時候設置一下環境.
// DoStringLuaFunction func = LoadString(luaScriptName, scriptEnv);LuaDataMgr.setfenv(func, scriptEnv);func.Call();3.利用名稱空間來自動配置屬性
Xlua需要配置屬性的地方很多,比如[Hotfix],[LuaCallCSharp]和[CSharpCallLua],對于delegate的配置,我建議自動化,不然以后想用的時候才發現沒配置,用不了就尷尬了.
[CSharpCallLua]public static List<Type> CSharpCallLua_Luoyinan{get{Type[] types = Assembly.Load("Assembly-CSharp").GetTypes();List<Type> list = (from type in typeswhere type.Namespace == "Luoyinan" && type.IsSubclassOf(typeof(Delegate)) select type).ToList();4.C#調用lua的接口管理
所有C#調用Lua的接口應該統一在一個類里面管理,這個類還應該實現一個緩存功能,防止每次調用都去從全局表Get.
[CSharpCallLua]public interface IMessageRegister{bool HasMessage(int messageId);string GetMessageName(int messageId);void Register(int messageId);} private static IMessageRegister mIMessageRegister;public static IMessageRegister iMessageRegister{get{if (mIMessageRegister == null)mIMessageRegister = LuaBehaviour.luaEnv.Global.Get<IMessageRegister>("MessageRegister");return mIMessageRegister;}}5.hotfix熱修復
熱修復主要遇到兩個問題,一個是回調函數的使用,要用一個閉包封裝一下,傳self.
一個是對hotfix函數的統一清除.如果你需要熱重載lua,這個是很有必要的,
--封裝一下hotfix,增加記錄功能,這樣我們好統一清除hotfix hotfixed = {} local org_hotfix = xlua.hotfix xlua.hotfix = function(cs, field, func)local tbl = (type(field) == 'table') and field or {[field] = func}hotfixed[cs] = tblorg_hotfix(cs, field, func) end--清除所有hotfix function clear_all_hotfix()for k, v in pairs(hotfixed) dofor i, j in pairs(v) doxlua.hotfix(k, i, nil) print("clear_all_hotfix : ", i)endendhotfixed = {} end6.GC問題
? ? ? ?xlua上手還是很快的,但是要用好就沒那么簡單,要了解里面一些底層原理,才能避免一些坑,比如GC問題.lua是一門動態語言,函數參數可以任意類型,任意個數,返回值也可以任意類型,任意個數,在C#的接口可能要這么寫:object[] Call(params?object[] args),用object來轉換,就會有boxing了.如何避免這種GC呢,只要明確參數類型和個數就行,一個個參數的壓棧,調用完一個個返回值的取,具體來說,就是生成代碼.加了[LuaCallCSharp]后,就可以生成代碼了,但是你可能沒把所有的代碼都加上[LuaCallCSharp],這些沒生成代碼的,也能調用,會走反射調用,然后參數的傳遞,就是object[]這種.有大量GC.所以如果你有一個沒生成代碼的類(你覺得很少調用就沒生成),但在Update里面每幀都調用了,哪怕只是一個property的訪問,都會產生嚴重的gc.對于這種情況,我們要做的是用編輯器的profiler來查看GC情況,如果發現漏掉的,就趕緊加上[LuaCallCSharp]
? ? ?至于其他的調用怎么避免GC,請參考xlua文檔.
7.代碼裁剪
? ? ? Unity引擎有個代碼裁剪的選項,引擎沒用到的接口,都會被裁減掉,優化效率.是否裁剪的標準,是看C#里面用到沒,如果你lua用到了,但是C#沒用到,也會被裁剪掉,因為C#這邊不知道你lua用到了.如果是生成了代碼的接口,不會被裁剪,因為用到了,但是那些反射調用的就可能會.如果要解決這個問題,可以加上[ReflectionUse],或者你關掉Unity的裁剪優化,我建議關掉裁剪優化,這樣你在hotfix的時候,就可以調用引擎任何代碼了.
8.內存泄漏問題
? ? ?現在Unity主流的lua解決方案,不管是xlua,ulua,slua,如果使用不當,都潛在嚴重的內存泄漏風險,這不是危言聳聽.這是lua和C#交互的設計原理引起的.
? ? ? C#對象在lua側都是userdata,C#對象Push到lua,是通過dictionary將lua的userdata和C#對象關聯起來的,這個dictionary起到一個緩存和查找的作用.只要lua中的userdata沒回收,c# object也就會被這個dictionary拿著引用,導致無法回收。最常見的就是gameobject和component,如果lua里頭引用了他們,即使你進行了Destroy,也會發現C#側他們還殘留著,這就是內存泄漏。想要立馬清理干凈,就得先手動調用lua gc,xlua才會把這個引用關系從dictionary里面去掉.
? ? ? 理論上,lua會定期自動gc,來回收這個userdata吧,底層細節應該不需要我們上層的使用者來操心,但是這個自動gc并不靠譜,因為lua的增量gc是以lua的內存為參考,可能lua的內存只增加很少的情況下,C#那邊的內存卻增加了幾十M.實際的使用情況也證明了這點,導致了大量的內存泄漏.
? ? ? 所以,我能想到的辦法就是手動管理,lua的自動gc不能知道C#側的內存增量情況,但是我們知道啊,所以應該找一個合適的時機手動調用lua gc,再銷毀C#對象,再調用C#的gc,比如切換場景的時候,或者關閉銷毀一個UI界面的時候.
? ? ? 如何發現自己的項目是否存在這種內存泄漏呢?監控這個dictionary就行了,xlua就是的ObjectTranslator類的reverseMap,如果你反復切換場景,這個reverseMap的數量一直在漲,那就發生內存泄漏了.
9.性能問題
? ? ? ?lua的性能比C#差很多,但是真正影響性能的地方是過多地在lua中調用c#.在lua中引用一個C#對象的代價是昂貴的,如果有必要,可以封裝一些接口減少這種調用,比如你在lua側引用了一堆C#對象,然后計算好一個值,再設置回去.就不如直接封裝一個簡單的直接設置的接口.一般在lua的每幀調用的update函數中,應該做極致的性能優化,優化方法也會多,核心的優化原則就是減少C#對象的引用和一些參數的傳遞.比如你要給一個C#服務器對象設置位置,你直接在lua側引用這個C#對象,再賦值回去,就不如封裝一個設置位置的接口,傳遞serverId和位置x, y,z回去.具體的設置操作就在C#側完成.
10.lua加載
? ? ?單個lua文件的加載是同步加載,用到再加載和編譯,代碼相互require關聯過多,就可能同時加載多個lua文件,引起卡頓的,因為你的lua文件是文本的,加載比較耗時.所以我們后來放棄這種方式了.
? ? ?如果打包成一個lua包,用lz4壓縮格式,加載速度就快很多.打成一個lua包以后,還可以對包加密成一個二進制文件,再打包.
加密包解包的時候,就需要用到AssetBundle.LoadFromMemory函數了
AssetBundle ab = AssetBundle.LoadFromFile(bundlePath);TextAsset textAsset = ab.LoadAsset<TextAsset>(BundleManager.luaAbPath.ToLower());if (textAsset == null){LogSystem.DebugLog("decrypt. {0}包沒這個文件: {1}", BundleManager.luaAbName, BundleManager.luaAbPath.ToLower());return null;}ab.Unload(false);byte[] data = textAsset.bytes;data = Util.Decrypt(data);LuaBehaviour.mCacheAb = AssetBundle.LoadFromMemory(data);好了,xlua的分享暫時就這些吧.
總結
以上是生活随笔為你收集整理的Unity3D手游项目的总结和思考(6) - Xlua的使用心得的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html中获取浏览器窗口宽度,JavaS
- 下一篇: 你了解眼角膜移植术吗?哪些眼疾需要接受角