内容简介:有一个项目做完快上线了,不是lua写的,能热更新的东西就特别少,如果遇到bug也很难在第一时间热修复,所以我就接入了Xlua这个插件原本只是想热修复一下的,后来领导要求把逻辑系统的C#代码全部换成了Lua,至于为什么,因为他们习惯了每天都更新和修改的开发模式...所以我们干了一件极其丧心病狂的事情,就是逻辑系统的C#代码全部翻译成了lua代码,全手动翻译...我保证,打死以后也不会再干类似的事情...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 initialization void Start () { if (luaStart != null) { luaStart(); } } // Update is called once per frame void 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,我们后续手动调用Awake if (string.IsNullOrEmpty(luaScriptName)) return;
public void OnEnable() { // 动态挂接LuaBehaviour,第一次OnEnable调用的时候luaScriptName还未设置,是null,直接return,我们后续手动调用第一次的OnEnable if (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,下次重复使用,使用的时候设置一下环境.
// DoString LuaFunction 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 types where 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.
--用闭包封装一下,用于需要传self的回调 function handler(obj, method) return function(...) if not method then print("method == nil " .. obj); end return method(obj, ...) end end
一个是对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] = tbl org_hotfix(cs, field, func) end --清除所有hotfix function clear_all_hotfix() for k, v in pairs(hotfixed) do for i, j in pairs(v) do xlua.hotfix(k, i, nil) print("clear_all_hotfix : ", i) end end hotfixed = {} end
6.GC问题
xlua上手还是很快的,但是要用好就没那么简单,要了解里面一些底层原理,才能避免一些坑,比如GC问题.lua是一门动态语言,函数参数可以任意类型,任意个数,返回值也可以任意类型,任意个数,在C#的接口可能要这么写: object [] Call( params object [] args) ,用object来转换,就会有boxing了.如何避免这种GC呢,只要明确参数类型和个数就行, 一个个参数的压栈,调用完一个个返回值的取 ,具体来说,就是生成代码.加了[LuaCallCSharp]后,就可以生成代码了,但是你可能没把所有的代码都加上 [LuaCallCSharp],这些没生成代码的,也能调用,会走反射调用,然后参数的传递,就是object[]这种.有大量GC.所以如果你有一个没生成代码的类(你觉得很少调用就没生成),但在Update里面每帧都调用了,哪怕只是一个proterty的访问,都会产生严重的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的分享暂时就这些吧.
作者:qq18052887 发表于 2018/05/21 17:57:59原文链接 https://blog.csdn.net/qq18052887/article/details/80390425
阅读:5
以上所述就是小编给大家介绍的《Unity3D手游项目的总结和思考(6) - Xlua的使用心得》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript & jQuery
David Sawyer McFarland / O Reilly / 2011-10-28 / USD 39.99
You don't need programming experience to add interactive and visual effects to your web pages with JavaScript. This Missing Manual shows you how the jQuery library makes JavaScript programming fun, ea......一起来看看 《JavaScript & jQuery》 这本书的介绍吧!