找回密码
 立即注册
查看: 291|回复: 0

Unity3D手游项目的总结和思考(6) - Xlua的使用心得

[复制链接]
发表于 2021-8-14 14:36 | 显示全部楼层 |阅读模式
有一个项目做完快上线了,不是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,官方提供了一个例子,但只是告诉你一个实现思路,真要在项目中用起来,有些地方还得改进才行.
官方例子:
  1. using UnityEngine;
  2.      using System.Collections;
  3.      using System.Collections.Generic;
  4.      using XLua;
  5.      using System;
  6.      
  7.       [
  8.       System.Serializable]
  9.      publicclassInjection
  10.       {
  11.      publicstring name;
  12.      public GameObject
  13.       value;
  14.      
  15.       }
  16.      
  17.       [
  18.       LuaCallCSharp]
  19.      publicclassLuaBehaviour :
  20.       MonoBehaviour {
  21.      public TextAsset luaScript;
  22.      public Injection[] injections;
  23.      internalstatic LuaEnv luaEnv =
  24.       new LuaEnv();
  25.       //all lua behaviour shared one luaenv only!internalstaticfloat lastGCTime =
  26.       0;
  27.      internalconstfloat GCInterval =
  28.       1;
  29.       //1 second private Action luaStart;
  30.      private Action luaUpdate;
  31.      private Action luaOnDestroy;
  32.      private LuaTable scriptEnv;
  33.      voidAwake()
  34.           {
  35.      
  36.               scriptEnv = luaEnv.NewTable();
  37.      
  38.               LuaTable meta = luaEnv.NewTable();
  39.      
  40.               meta.Set(
  41.       "__index", luaEnv.Global);
  42.      
  43.               scriptEnv.SetMetaTable(meta);
  44.      
  45.               meta.Dispose();
  46.      
  47.               scriptEnv.Set(
  48.       "self12",
  49.       this);
  50.      foreach (
  51.       var injection
  52.       in injections)
  53.      
  54.               {
  55.      
  56.                   scriptEnv.Set(injection.name, injection.
  57.       value);
  58.      
  59.               }
  60.      
  61.               scriptEnv.Set(
  62.       "transform", transform);
  63.      
  64.               luaEnv.DoString(luaScript.text,
  65.       "LuaBehaviour", scriptEnv);
  66.      
  67.               Action luaAwake = scriptEnv.Get<Action>(
  68.       "awake");
  69.      
  70.               scriptEnv.Get(
  71.       "start",
  72.       out luaStart);
  73.      
  74.               scriptEnv.Get(
  75.       "update",
  76.       out luaUpdate);
  77.      
  78.               scriptEnv.Get(
  79.       "ondestroy",
  80.       out luaOnDestroy);
  81.      if (luaAwake !=
  82.       null)
  83.      
  84.               {
  85.      
  86.                   luaAwake();
  87.      
  88.               }
  89.      
  90.           }
  91.      // Use this for initializationvoidStart ()
  92.           {
  93.      if (luaStart !=
  94.       null)
  95.      
  96.               {
  97.      
  98.                   luaStart();
  99.      
  100.               }
  101.      
  102.               }
  103.      // Update is called once per framevoidUpdate ()
  104.           {
  105.      if (luaUpdate !=
  106.       null)
  107.      
  108.               {
  109.      
  110.                   luaUpdate();
  111.      
  112.               }
  113.      if (Time.time - LuaBehaviour.lastGCTime > GCInterval)
  114.      
  115.               {
  116.      
  117.                   luaEnv.Tick();
  118.      
  119.                   LuaBehaviour.lastGCTime = Time.time;
  120.      
  121.               }
  122.      
  123.               }
  124.      voidOnDestroy()
  125.           {
  126.      if (luaOnDestroy !=
  127.       null)
  128.      
  129.               {
  130.      
  131.                   luaOnDestroy();
  132.      
  133.               }
  134.      
  135.               luaOnDestroy =
  136.       null;
  137.      
  138.               luaUpdate =
  139.       null;
  140.      
  141.               luaStart =
  142.       null;
  143.      
  144.               scriptEnv.Dispose();
  145.      
  146.               injections =
  147.       null;
  148.      
  149.           }
  150.      
  151.       }
  152.      一.lua脚本用TextAsset来保存是不行的,因为这种的话,就会把lua文件打包进prefab里面.lua和prefab需要解耦,那么保存一个lua文件名字是更好的办法.用到的时候,再根据名字加载.二.动态挂接这个脚本的问题,在prefab上静态挂接这个脚本没有这个问题,但是如果要在代码中动态挂接这个脚本就有问题,Awake初始化的时候,并没有设置lua脚本的名字,无法加载lua文件.解决办法有两种,一种是先隐藏挂脚本的游戏对象,挂上去后,设置好lua脚本名字再激活,这样的坏处是,隐藏和激活可能会影响脚本逻辑.另外一种完美的办法是,挂脚本后,自动调用的Awake和OnEnable跳过,设置好lua名字后,再手动调用publicvoidAwake()
  153.           {
  154.      // 动态挂接LuaBehaviour,Awake调用的时候luaScriptName还未设置,是null,直接return,我们后续手动调用Awakeif (
  155.       string.IsNullOrEmpty(luaScriptName))
  156.      return;
  157.      publicvoidOnEnable()
  158.           {
  159.      // 动态挂接LuaBehaviour,第一次OnEnable调用的时候luaScriptName还未设置,是null,直接return,我们后续手动调用第一次的OnEnableif (
  160.       string.IsNullOrEmpty(luaScriptName))
  161.      return;
  162.      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 behaviourend
复制代码
三.重复初始化LuaBehaviour的性能问题
        如果你给10个怪物挂上一个LuaBehaviour,关联的都是同样一个monster.lua的脚本,那么这10个怪物每次初始化的DoString都会编译monster.lua...这会带来没必要的性能开销,其实只需要编译一次.如果只编译一次呢,用LoadString来替代,缓存LoadString返回的LuaFunction,下次重复使用,使用的时候设置一下环境.
  1. // DoString
  2.                LuaFunction func = LoadString(luaScriptName, scriptEnv);
  3.       
  4.                LuaDataMgr.setfenv(func, scriptEnv);
  5.       
  6.                func.Call();
  7.       
复制代码
3.利用名称空间来自动配置属性
Xlua需要配置属性的地方很多,比如[Hotfix],[LuaCallCSharp]和[CSharpCallLua],对于delegate的配置,我建议自动化,不然以后想用的时候才发现没配置,用不了就尴尬了.



  •           [      CSharpCallLua]     



  • publicstatic 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]     



    publicinterfaceIMessageRegister



  •           {     



  • boolHasMessage(int messageId);     



  • stringGetMessageName(int messageId);     



  • voidRegister(int messageId);     



  •           }     





  • privatestatic IMessageRegister mIMessageRegister;     



  • publicstatic IMessageRegister iMessageRegister     



  •               {     



    get



  •                   {     



  • if (mIMessageRegister ==       null)     



  •                           mIMessageRegister = LuaBehaviour.luaEnv.Global.Get<IMessageRegister>(      "MessageRegister");     



  • return mIMessageRegister;     



  •                   }     



  •               }     


5.hotfix热修复
热修复主要遇到两个问题,一个是回调函数的使用,要用一个闭包封装一下,传self.





  •       --用闭包封装一下,用于需要传self的回调     



    function handler(obj, method)



    returnfunction(...)



  • ifnot method then     



  • print("method == nil " .. obj);     



    end



    returnmethod(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里面每帧都调用了,哪怕只是一个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());     



  • returnnull;     



  •                           }     



  •                           ab.Unload(      false);     



  • byte[] data = textAsset.bytes;     



  •                           data = Util.Decrypt(data);     



  •                           LuaBehaviour.mCacheAb = AssetBundle.LoadFromMemory(data);     


好了,xlua的分享暂时就这些吧.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 11:50 , Processed in 0.158642 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表