有一个项目做完快上线了,不是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.

--用闭包封装一下,用于需要传self的回调
function handler(obj, method)return function(...)if not method thenprint("method == nil " .. obj);endreturn 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] = 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 = {}
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());return null;}ab.Unload(false);byte[] data = textAsset.bytes;data = Util.Decrypt(data);LuaBehaviour.mCacheAb = AssetBundle.LoadFromMemory(data);

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

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

  1. Unity3D手游项目的总结和思考(1) - 渲染技术

    有朋友私信我问我为啥很久不更新博客,是不是转行了...我当然不可能承认自己懒啊,只能回复说太忙了.不过项目开发中,确实很难有时间和心力去总结和思考一些东西,不过现在忙完一些项目以后,我又回来了. 渲染 ...

  2. 手游项目初期的一些想法

    公司要开始新的手游项目,需要我给程序这边做一个简单的介绍,在这里简单记录一下. 1.语言框架选择 cocos2d-x: 优点:cocos2d-x 是c++框架,成熟的引擎,有很多成功的例子,我叫mt, ...

  3. Unity3D手游开发实践

    本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身.虽然基于Unity3D,很多东西同样适用于Cocos.本文从以下10大点进行阐述:架构设计.原生插件/平台交互.版本与补丁.用脚本,还是不用? ...

  4. (转)Unity3D手游开发实践

    作者:吴秦 出处:http://www.cnblogs.com/skynet/ 本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接). (转 ...

  5. 从新手到老鸟的脱变过程之《如何做手游项目》

    老鸟飘过! 目前,新手的状态基本是这样的! 先是找群,加到群后,先看群文件有没有东西? 然后,再看群公告有没有辅助类的广告? 在群里默默的看着,有没有人发言? 或是提供有价值的信息? 特别希望看到,有 ...

  6. 游戏众筹风潮来袭 为何手游项目众筹却如此艰难

    何为游戏众筹?游戏众筹指的是游戏开发商通过互联网向游戏玩家展开资金筹集,进而使用所得的资金用来开发游戏.它的特征在于低门槛,无论何种身份.地位.职业.年龄,只要你有想法.有创造能力便能够发起游戏项目. ...

  7. 着手一个手游项目的思考

    虽然个人阅历有限,但也对端游,页游,手游都有涉及. 目前正值筹备新项目的时候,又面临着技术选型等方面的问题.记录在此,以整理思绪 技术选型 1.前后端的技术选择 前端我觉得要按以下方向来  平台-〉3 ...

  8. [unity3d]手游资源热更新策略探讨

    原地址:http://blog.csdn.net/dingxiaowei2013/article/details/20079683 我们学习了如何将资源进行打包.这次就可以用上场了,我们来探讨一下手游 ...

  9. 腾讯Unity3D手游 dll加密分析

    此文章的技术内容已过时,仅当做技术存档参考.腾讯的Unity游戏已不再mono.so中加密dll了,而是通过libtprt.so来hook了加载Assembly-CSharp.dll的地方进行解密,解 ...

最新文章

  1. ES6 Rest参数
  2. 怎么能把看不清的照片给看清_哪些人会得?应该怎么治?关于白内障的“经典十问”...
  3. IcmpBackDoor
  4. Ubuntu环境下TensorFlow 的环境搭建(二)安装TensorFlow(CPU版)
  5. oracle exp/imp命令详解
  6. 此应用无法在你的电脑上运行_苹果自研电脑芯片 M1 配合 macOS 11,可直接运行 iOS 应用...
  7. TCC事务补偿机制实现分布式事务控制介绍
  8. “一次编写,随处运行” Intel HTML5技术研讨会
  9. spring基础整理
  10. ubuntu安装c/c++编译环境
  11. java发送post请求json格式_Linux QT 4G发送HTTP POST请求发送JSON格式的数据
  12. 关于关闭Eclipse的控制台自动跳出
  13. 除了敲代码,程序员还需要哪些必备技能?
  14. 杭州有哪些牛批的互联网公司
  15. 嵌入式系统设计师考试完美笔记
  16. Go: panic / recover 简介与实践
  17. Xceed Words for .NET转换和页眉/页脚功能
  18. 模拟通讯录系统2.0
  19. 小啊呜产品读书笔记001:《邱岳的产品手记-11》第21讲 产品案例分析:Fabulous的精致养成
  20. linux安装Redis保姆级别

热门文章

  1. [QML开发笔记]-QML滑屏效果
  2. 奥沙利文独家授权数字藏品正式发售 线上线下搭配多款专属福利
  3. Android攻城狮ViewFlipper
  4. 顶刊实证复现:排污权交易机制是否提高了企业全要素生产率 (思路梳理+全数据源+python代码)
  5. 基于JAVA高校多媒体设备运维管理系统服务端计算机毕业设计源码+数据库+lw文档+系统+部署
  6. HDS发布视频分析软件HVA 为企业提供运营智能与安全洞察
  7. C#中将图片的背景去除
  8. 年终总结 | 怎样识别并投资高效能人才?
  9. alios 系统配置转存到一个头文件
  10. HCPL3120手册翻译