这是什么东西

  前阵子刚刚集成xlua到项目,目的只有一个:对线上游戏C#逻辑有Bug的地方执行修复,通过考察了xlua和tolua,最终选择了xlua,原因如下:

  1)项目已经到了后期,线上版本迭代了好几次,所以引入Lua的目的不是为了开发新版本模块,而是修复旧版本Bug。

  2)修复Bug针对的是iOS平台,总所周知,安卓平台是可以通过更新DLL去修复的,而修复Bug直接改写C#代码肯定要比整个函数翻译为Lua来得简单,实际上最后由于xlua注入和我们项目的安卓代码加密流程冲突,我们对安卓保留了原先的热更流程,而对iOS平台才执行xlua热更。

  xlua在我们的这种情况下很是适用,如xlua作者所说,用C#开发,用lua热更,xlua这套框架为我们提供了诸多便利,至少我可以说,在面临同样的情况下,你用tolua去做同样的事情是很费心的。同样,在读的各位,如果你是想用xlua做整套客户端游戏逻辑的,这篇文对你可能就没什么借鉴意义了,这里提前打个招呼,毕竟大家时间都有限的,我也不想你读完后才发现我在坑你。其实关于纯lua写游戏逻辑的整套框架,我目前正在开发之中,如果到时候觉得有必要,有时间再适当做做分享。不过,这套正在做的框架我选择了tolua。其实纯lua写逻辑,使用xlua还是tolua并不是那么重要,因为与c#交互会少很多,而且一般都是耗性能的地方才放c#,你一次A*寻路的开销,相比一次lua到c#调用的开销是不知道大到哪里去的,所以即使网上有各种lua框架性能的评测,其实我感觉意义都不太大,如果真要频繁调用,那不管xlua还是tolua你都要考虑方案去优化的。这个话题以后有时间再去展开好了。

  当时在做完这个xlua热更框架,我还打算写篇博文分享一下,因为经常看到xlua群里面网友问各种问题,其实原因说起来只有一个,因为大家对xlua集成到项目没有实战经验,你只要尝试过,很多问题都没有想象中的那么困难。后来,由于工作一直比较忙,忙着另外一个游戏的网络同步优化,还有近期正在做的上面说的这套全新的lua框架,这个事情就被搁浅了下来;还有另一层面的原因是因为做完这个框架以后,发现其实大部分都是在熟悉xlua,而自己写的代码少得可伶,感觉也没什么太多要分享的地方。毕竟热修复,本质上来说就是一个轻量级的东西。

  最近,趁着周末闲来无事,我又去网上找了下xlua相关的博客、github分享,我不知道你们有做过xlua引入项目进行热修复的人,对这些分享是什么感受,总是,我个人是发现没有一个是我能够得到太多参考价值的,其实这也和xlua的框架特性有关。它的核心理念是C#写逻辑,xlua热修复,除非你是新开的项目,一开始就遵循xlua热更的各种规范。而如果你是后期引入的xlua,那么,xlua热修复代码的复杂度,很大程度上取决于你框架原先c#代码的写法,我这里举一个例子,你们大概就能清楚我到底在说什么:

  比如说委托的使用,在c#侧经常作为回调去使用,xlua的demo里对委托的热修复示例是这样的:

 

 1 public Action<string> TestDelegate = (param) =>
 2 {
 3     Debug.Log("TestDelegate in c#:" + param);
 4 };
 5 
 6 public void TestFunction(Action<string> callback)
 7 {
 8     //do something
 9     callback("this is a test string");
10     //do something
11 }
12 
13 public void TestCall()
14 {
15     TestFunction(TestDelegate);
16 }

 

  这里相当于把委托定义为了成员变量,那么你在lua侧,如果要热修复TestCall函数,要将这个委托作为回调传递给TestFunction,只需要使用self.TestDelegate就能访问,很简单。而问题就在于,我们项目之前对委托的使用方式是这样的:

 

 1 public void TestDelegate(String param)
 2 {
 3     Debug.Log("TestDelegate in c#:" + param);
 4 }
 5 
 6 public void TestFunction(Action<string> callback)
 7 {
 8     //do something
 9     callback("this is a test string");
10     //do something
11 }
12 
13 public void TestCall()
14 {
15     TestFunction(TestDelegate);
16 }

 

  那么问题就来了,这个TestDelegate是一个函数,在调用的时候才自动创建了一个临时委托,那么Lua侧,你就没办法简单地去热更了,怎么办?清楚了吧,我说的就是类似这样的一些问题,因为一开始没有考虑过进行xlua热更,所以导致没有明确匹配xlua热更规则的相关代码规范,从而xlua热修复困难。

  这个例子可能举得不是太好,你可以暴力修改项目中所有这样写法的地方,可是我很不愿意这样去对旧代码进行大改,不仅工作量大而且还风险大,谁知道会不会为了修复Bug引入另外的Bug,你说是不是?另外,下面的这种写法其实有GC问题,很不推荐,这个问题是项目历史遗留下来的,因为早期开发这个游戏的人并没有注重GC问题,关于GC,后面有时间我再去展开讨论,这里也不再多说。

  总之,通过这么一个例子,我首先想要大家弄明白的一点的是这篇博客在讨论什么东西,所以,如果你如果初识xlua框架,也面临同样的境况,那接下来的内容就值得一读。

 现行xlua分享的弊端

  上面说了,我本来是不打算浪费精力写这篇博客的,但是翻阅了giehub上start排前的几个项目,发现坑不是一般的多,实在是看不下去了,所以才打算提笔写写我遇到过的与解决过的坑。现行所谓xlua分享大多都不着要点,没有直接命中大部分人所面临的问题,初步缺陷有如下几点:

  1)体积太重:一些所谓xlua热更框架,集成了各种所谓资源热更新、场景管理、音乐管理、定时器管理等等模块。我要做游戏框架,我不会用你的这些模块;我要做xlua热更,我找不到我想要的东西。

  2)避重就轻:有些分享框架,把xlua集成过来,然后自己用NGUI或者UGUI写了个小场景,然后,你看下去发现全是Demo的c#代码,这是什么玩意?

  3)不着要点:有些甚至是c#框架+lua框架,我初步浏览也是没看明白xlua在里面到底扮演了什么角色。

  有关这些东西,我就不多吐槽了,总之一句话,商用借鉴价值,不大。

轻量级xlua热修复框架

  与其说是框架,不如说这只是xlua进行热修复的一个演示demo,或者说xlua的一个扩展,对xlua很多没有提供的一些外围功能进行了相关扩展。xlua的设计还是挺不错的,包括整个架构,也保持了简洁性,不带太多多余的东西,包括第三方库这点就可以看出。一个框架的设计理念很重要,什么东西该放进来,什么东西不需要考虑,有取舍才能够简洁,而简洁的东西用起来才清爽。

框架工程结构

  我假设你已经清楚了xlua做热修复的流程,包括xlua工作原理,怎么加载lua脚本,怎么去做lua脚本打包和热更,lua热工脚本是怎样的工作流程等内容,因为这些太过基础,我不会讲述太多,如果你不清楚,自己去补补功课。下面给先一张工程截图,让大家有个总体认识:

 

xlua热修复框架工程结构

 

  1)Scripts/xlua/XLuaManager:xlua热修复环境,包括的luastate管理,自定义loader,就这么些东西。

  2)Resources/xlua/Main.lua:xlua热修复入口

  3)Resources/xlua/Common:提供给lua代码使用的一些工具方法,大部分所遇到的工程问题得到的统一解决方案,都写在这里,提供lua逻辑代码到C#调用的一层封装

  4)Scripts/xlua/Util:为xlua的lua脚本提供的C#侧代码支持,被Resources/xlua/Common所使用

  5)Scripts/test/HotfixTest:需要热修复的c#脚本,由于时间有限,我并没有去单独写示例,这些都是从项目中抽离出来的脚本,不可运行,只是为了让大家对比热修复的C#代码才放上来的

  6)Resources/xlua/HotFix:热修复脚本,同样,无法直接运行,但是大部分问题lua代码都在其中,对比被修复的C#代码,就能抓住要点。

  可以看到,很轻量的一个东西,全为xlua做热修复量身定制,没有任何多余的东西。另外,由于时间有限,我并没有把所有自己项目使用到的东西提供出来,刚刚也说了,xlua怎么做热修复,会遇到什么样的问题,和你本身的C#代码有很大关系,而各位自己的项目,肯定有很多东西和我们项目不一样,所以我觉得完全没有必要去弄全。而且,授人鱼不如授人以渔,这里侧重说几点重要的问题,和它们的解决思路,举一反三,大家应该就知道怎么去做好热更了。这里主要说的方向有这么几点:

  1)消息系统:打通cs和lua侧的消息系统,其中的关键问题是泛型委托

  2)对象创建:怎么样在lua侧创建cs对象,特别是泛型对象

  3)迭代器:cs侧列表、字典之类的数据类型,怎样在lua侧泛型迭代

  4)协程:cs侧协程怎么热更,怎么在lua侧创建协程

  5)委托作为回调:cs侧函数用作委托回调当作函数调用时的形参时,怎样在lua侧传递委托形参

  Scripts/xlua/Util与Resources/xlua/Common这两个目录下的脚本,基本上来说,已经很好的说明了怎么样针对自己项目的具体情况对xlua进行扩展,虽然这里的脚本不是面面俱到,但是这里组织的结构我想应该是可以适应大多数问题的解决方式的。下面就上面几点着重分析一下。

lua侧cs泛型对象创建

  先从简单的说起,对象创建xlua给的例子很简单,直接new CS.XXX就好,但是如果你要创建一个泛型List对象,比如List<string>,要怎么弄?你可以为List<sting>在c#侧定义一个静态辅助类,提供类似叫CreateListString的函数去创建,但是你不可能为所有的类型都定义这样一层包装吧。所以,问题的核心是,我们怎么样在Lua侧只知道类型信息,就能让cs代劳给我们创建出对象,下面看代码:

 

 1 --common.helper.lua
 2 -- new泛型array
 3 local function new_array(item_type, item_count)
 4     return CS.XLuaHelper.CreateArrayInstance(item_type, item_count)
 5 end
 6 
 7 -- new泛型list
 8 local function new_list(item_type)
 9     return CS.XLuaHelper.CreateListInstance(item_type)
10 end
11 
12 -- new泛型字典
13 local function new_dictionary(key_type, value_type)
14     return CS.XLuaHelper.CreateDictionaryInstance(key_type, value_type)
15 end

 

  这是Resources/xlua/Common下的helper脚本其中的一部分,接下来的脚本我都会在开头写上模块名,不再做说明。之前说过,这个目录下的代码为lua逻辑代码提过对cs代码访问的桥接,这样做有两个好处:第一个是隐藏实现细节,第二个是容易更改实现。这里的三个接口都使用到了Scripts/xlua/Util下的XLuaHelper来做真实的事情。这两个目录下的脚本大概的职责都是这样的,Resources/xlua/Common封装lua调用,如果能用lua脚本实现,那就实现,不能实现,那在Resources/xlua/Common写cs脚本提供支持。下面看cs侧相关代码:

 

 1 // CS.XLuaHelper
 2 // 说明:扩展CreateInstance方法
 3 public static Array CreateArrayInstance(Type itemType, int itemCount)
 4 {
 5     return Array.CreateInstance(itemType, itemCount);
 6 }
 7 
 8 public static IList CreateListInstance(Type itemType)
 9 {
10     return (IList)Activator.CreateInstance(MakeGenericListType(itemType));
11 }
12 
13 public static IDictionary CreateDictionaryInstance(Type keyType, Type valueType)
14 {
15     return (IDictionary)Activator.CreateInstance(MakeGenericDictionaryType(keyType, valueType));
16 }

 

  这里并没有太多要说的东西,如果你对cs的API不熟悉,那msdn查就好,主要是提供一个代码结构和对类似问题的一个通用解决方式给大家参考。

lua侧cs迭代器访问

  这个问题其实也很简单,xlua作者在demo中也给出了示例,只是我觉得麻烦,想要lua侧通用一点的语法,所以包装了一层语法糖,同样,lua代码如下:

 

 1 -- common.helper.lua
 2 -- cs列表迭代器:含包括Array、ArrayList、泛型List在内的所有列表
 3 local function list_iter(cs_ilist, index)
 4     index = index + 1
 5     if index < cs_ilist.Count then
 6         return index, cs_ilist[index]
 7     end
 8 end
 9 
10 local function list_ipairs(cs_ilist)
11     return list_iter, cs_ilist, -1
12 end
13 
14 -- cs字典迭代器
15 local function dictionary_iter(cs_enumerator)
16     if cs_enumerator:MoveNext() then
17         local current = cs_enumerator.Current
18         return current.Key, current.Value
19     end
20 end
21 
22 local function dictionary_ipairs(cs_idictionary)
23     local cs_enumerator = cs_idictionary:GetEnumerator()
24     return dictionary_iter, cs_enumerator
25 end

 

  这部分代码不需要额外的cs脚本提供支持,这里只是实现了lua的泛型迭代,能够用在lua的for循环中,使用代码如下(只给出列表示例,对字典是类似的):

 

 1 -- common.helper.lua
 2 -- Lua创建和遍历泛型列表示例
 3 local helper = require 'common.helper'
 4 local testList = helper.new_list(typeof(CS.System.String))
 5 testList:Add('111')
 6 testList:Add('222')
 7 testList:Add('333')
 8 print('testList', testList, testList.Count, testList[testList.Count - 1])
 9 
10 -- 注意:循环区间为闭区间[0,testList.Count - 1]
11 -- 适用于列表子集(子区间)遍历
12 for i = 0, testList.Count - 1 do
13     print('testList', i, testList[i])
14 end
15 
16 -- 说明:工作方式与上述遍历一样,使用方式上雷同lua库的ipairs,类比于cs的foreach
17 -- 适用于列表全集(整区间)遍历,推荐,很方便
18 -- 注意:同cs的foreach,遍历函数体不能修改i,v,否则结果不可预料
19 for i, v in helper.list_ipairs(testList) do
20     print('testList', i, v)
21 end

 

  要看懂这部分的代码,需要知道lua中的泛型for循环是怎么样工作的:

 

1 for var_1, ..., var_n in explist do 
2     block 
3 end

 

  对于如上泛型for循环通用结构,其代码等价于:

 

1 do
2     local _f, _s, _var = explist
3     while true do
4         local var_1, ... , var_n = _f(_s, _var)
5         _var = var_1
6         if _var == nil then break end
7         block
8     end
9 end

 

  泛型for循环的执行过程如下:
  首先,初始化,计算 in 后面表达式的值,表达式应该返回范性 for 需要的三个值:迭代函数_f,状态常量_s和控制变量_var;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  第二,将状态常量_s和控制变量_var作为参数调用迭代函数_f(注意:对于 for 结构来说,状态常量_s没有用处,仅仅在初始化时获取他的值并传递给迭代函数_f)。
  第三,将迭代函数_f返回的值赋给变量列表。
  第四,如果返回的第一个值为 nil 循环结束,否则执行循环体。
  第五,回到第二步再次调用迭代函数。

  如果控制变量的初始值是 a0,那么控制变量将循环:a1=_f(_s,a0)、a2=_f(_s,a1)、……,直到 ai=nil。对于如上列表类型的迭代,其中explist = list_ipairs(cs_ilist),根据第一点,可以得到_f = list_iter,_s = cs_ilist, _var = -1,然后进入while死循环,此处每次循环拿_s = cs_ilist, _var = -1作为参数调用_f = list_iter,_f = list_iter内部对_var执行自增,所以这里的_var就是一个计数变量,也是list的index下标,返回值index、cs_ilist[index]赋值给for循环中的i、v,当遍历到列表末尾时,两个值都被赋值为nil,循环结束。这个机制和cs侧的foreach使用迭代器的工作机制是有点雷同的,如果你清楚这个机制,那么这里的原理就不难理解。

lua侧cs协程热更

  这里先看cs侧协程的用法:

 

 1 // cs.UIRankMain
 2 public override void Open(object param, UIPathData pathData)
 3 {
 4     // 其它代码省略
 5     StartCoroutine(TestCorotine(3));
 6 }
 7 
 8 IEnumerator TestCorotine(int sec)
 9 {
10     yield return new WaitForSeconds(sec);
11     Logger.Log(string.Format("This message appears after {0} seconds in cs!", sec));
12     yield break;
13 }

 

  这是很普通的一种协程写法,下面对这个协程的调用函数Open,协程函数TestCorotine执行热修复:

 

 1 -- HotFix.UIRankMainTest.lua
 2 -- 模拟Lua侧的异步回调
 3 local function lua_async_test(seconds, coroutine_break)
 4     print('lua_async_test '..seconds..' seconds!')
 5     -- TODO:这里还是用Unity的协程相关API模拟异步,有需要的话再考虑在Lua侧实现一个独立的协程系统
 6     yield_return(CS.UnityEngine.WaitForSeconds(seconds))
 7     coroutine_break(true, seconds)
 8 end
 9 
10 -- lua侧新建协程:本质上是在Lua侧建立协程,然后用异步回调驱动,
11 local corotineTest = function(self, seconds)
12     print('NewCoroutine: lua corotineTest', self)
13     
14     local s = os.time()
15     print('coroutine start1 : ', s)
16     -- 使用Unity的协程相关API:实际上也是CS侧协程结束时调用回调,驱动Lua侧协程继续往下跑
17     -- 注意:这里会在CS.CorotineRunner新建一个协程用来等待3秒,这个协程是和self没有任何关系的
18     yield_return(CS.UnityEngine.WaitForSeconds(seconds))
19     print('coroutine end1 : ', os.time())
20     print('This message1 appears after '..os.time() - s..' seconds in lua!')
21     
22     local s = os.time()
23     print('coroutine start2 : ', s)
24     -- 使用异步回调转同步调用模拟yield return
25     -- 这里使用cs侧的函数也是可以的,规则一致:最后一个参数必须是一个回调,回调被调用时表示异步操作结束
26     -- 注意:
27     --    1、如果使用cs侧函数,必须将最后一个参数的回调(cs侧定义为委托)导出到[CSharpCallLua]
28     --    2、用cs侧函数时,返回值也同样通过回调(cs侧定义为委托)参数传回
29     local boolRetValue, secondsRetValue = util.async_to_sync(lua_async_test)(seconds)
30     print('coroutine end2 : ', os.time())
31     print('This message2 appears after '..os.time() - s..' seconds in lua!')
32     -- 返回值测试
33     print('boolRetValue:', boolRetValue, 'secondsRetValue:', secondsRetValue)
34 end
35 
36 -- 协程热更示例
37 xlua.hotfix(CS.UIRankMain, 'Open', function(self, param, pathData)
38     print('HOTFIX:Open ', self)
39     -- 省略其它代码
40     -- 方式一:新建Lua协程,优点:可新增协程;缺点:使用起来麻烦
41     print('----------async call----------')
42     util.coroutine_call(corotineTest)(self, 4)--相当于CS的StartCorotine,启动一个协程并立即返回
43     print('----------async call end----------')
44     
45     -- 方式二:沿用CS协程,优点:使用方便,可直接热更协程代码逻辑,缺点:不可以新增协程
46     self:StartCoroutine(self:TestCorotine(3))
47 end)
48 
49 -- cs侧协程热更
50 xlua.hotfix(CS.UIRankMain, 'TestCorotine', function(self, seconds)
51     print('HOTFIX:TestCorotine ', self, seconds)
52     --注意:这里定义的匿名函数是无参的,全部参数以闭包方式传入
53     return util.cs_generator(function()
54         local s = os.time()
55         print('coroutine start3 : ', s)
56         --注意:这里直接使用coroutine.yield,跑在self这个MonoBehaviour脚本中
57         coroutine.yield(CS.UnityEngine.WaitForSeconds(seconds))
58         print('coroutine end3 : ', os.time())
59         print('This message3 appears after '..os.time() - s..' seconds in lua!')
60     end)
61 end)

 

  这里代码看起来有点复杂,但是实际上要说的点几乎没有,因为xlua作者已经到协程做了比较好的支持。关于lua协程的工作原理,由于篇幅和时间的关系,这里不再具体做介绍了。

lua侧创建cs委托回调

  这一点和接下来的消息系统,说起来都有点麻烦,我不会说得太细,如果听不懂,那说明你对xlua或者lua语言的熟悉程度还有待提高。这里回归的是篇头所阐述的问题,当cs侧某个函数的参数是一个委托,而调用方在cs侧直接给了个函数,在lua侧怎么去热更的问题,先给cs代码:

 

 1 // cs.UIArena
 2 private void UpdateDailyAwardItem(List<BagItemData> itemList)
 3 {
 4     if (itemList == null)
 5     {
 6         return;
 7     }
 8 
 9     for (int i = 0; i < itemList.Count; i++)
10     {
11         UIGameObjectPool.instance.GetGameObject(ResourceMgr.RESTYPE.UI, TheGameIds.UI_BAG_ITEM_ICON, new GameObjectPool.CallbackInfo(onBagItemLoad, itemList[i], Vector3.zero, Vector3.one * 0.65f, m_awardGrid.gameObject));
12     }
13     m_awardGrid.Reposition();
14 }

 

  这是UI上面普通的一段异步加载背包Item的Icon资源问题,资源层异步加载完毕以后回调到当前脚本的onBagItemLoa函数对UI资源执行展示。现在就这段代码执行一下热修复:

 

 1 -- HotFix.UIArenaTese.lua
 2 -- 回调热更示例(消息系统的回调除外)
 3 --    1、缓存委托
 4 --    2、Lua绑定(实际上是创建LuaFunction再cast到delegate),需要在委托类型上打[CSharpCallLua]标签--推荐
 5 --    3、使用反射再执行Lua绑定
 6 xlua.hotfix(CS.UIArena, 'UpdateDailyAwardItem', function(self, itemList)
 7     print('HOTFIX:UpdateDailyAwardItem ', self, itemList)
 8     
 9     if itemList == nil then
10         do return end
11     end
12     
13     for i, item in helper.list_ipairs(itemList) do
14         -- 方式一:使用CS侧缓存委托
15         local callback1 = self.onBagItemLoad
16         -- 方式二:Lua绑定
17         local callback2 = util.bind(function(self, gameObject, object)
18             self:OnBagItemLoad(gameObject, object)
19         end, self)
20         -- 方式三:
21         --    1、使用反射创建委托---这里没法直接使用,返回的是Callback<,>类型,没法隐式转换到CS.GameObjectPool.GetGameObjectDelegate类型
22         --    2、再执行Lua绑定--需要在委托类型上打[CSharpCallLua]标签
23         -- 注意:
24         --    1、使用反射创建的委托可以直接在Lua中调用,但作为参数时,必须要求参数类型一致,或者参数类型为Delegate--参考Lua侧消息系统实现
25         --    2、正因为存在类型转换问题,而CS侧的委托类型在Lua中没法拿到,所以在Lua侧执行类型转换成为了不可能,上面才使用了Lua绑定
26         --    3、对于Lua侧没法执行类型转换的问题,可以在CS侧去做,这就是[CSharpCallLua]标签的作用,xlua底层已经为我们做好这一步
27         --    4、所以,这里相当于方式二多包装了一层委托,从这里可以知道,委托做好全部打[CSharpCallLua]标签,否则更新起来很受限
28         --    5、对于Callback和Action类型的委托(包括泛型)都在CS.XLuaHelper实现了反射类型创建,所以不需要依赖Lua绑定,可以任意使用
29         -- 静态函数测试
30         local delegate = helper.new_callback(typeof(CS.UIArena), 'OnBagItemLoad2', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object))
31         delegate(self.gameObject, nil)
32         -- 成员函数测试
33         local delegate = helper.new_callback(self, 'OnBagItemLoad', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object))
34         local callback3 = util.bind(function(self, gameObject, object)
35             delegate(gameObject, object)
36         end, self)
37         
38         -- 其它测试:使用Lua绑定添加委托:必须[CSharpCallLua]导出委托类型,否则不可用
39         callback5 = callback1 + util.bind(function(self, gameObject, object)
40             print('callback4 in lua', self, gameObject, object)
41         end, self)
42         
43         local callbackInfo = CS.GameObjectPool.CallbackInfo(callback3, item, Vector3.zero, Vector3.one * 0.65, self.m_awardGrid.gameObject)
44         CS.UIGameObjectPool.instance:GetGameObject(CS.ResourceMgr.RESTYPE.UI, CS.TheGameIds.UI_BAG_ITEM_ICON, callbackInfo)
45     end
46     self.m_awardGrid:Reposition()
47 end)

 

  有三种可行的热修复方式:

  1)缓存委托:就是在cs侧不要直接用函数名来作为委托参数传递,之前也说了,这里会临时创建一个委托,所以,最好在cs侧用函数初始化一个成员变量委托并缓存下来,使用的使用直接self.xxx传递委托到参数即可。

  2)Lua绑定:创建一个闭包,需要在cs侧的委托类型上打上[CSharpCallLua]标签,实际上xlua作者建议将工程中所有的委托类型打上这个标签,打上标签的委托xlua会生成一张映射表,当lua函数作为委托传递给cs函数用时,会自动去查表做映射。

  3)使用反射再执行lua绑定:这种方式使用起来很受限,这里不再做说明,要了解的朋友自己参考源代码中的注释或者自行测试体会。

 

打通lua和cs的消息系统

  这部分是最难说清楚的一点,也算是很有价值的一个参考方案。cs侧的这个消息系统参考:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger。这里面使用了泛型编程的思想,xlua作者在demo中针对泛型接口的热修复给出的建议是实现扩展函数,但是扩展函数需要对一个类型去做一个接口,这里的消息系统类型完全是可以任意的,甚至是自定义类型,显然这种方案显得捉襟见肘。核心的问题只有一个,怎么根据参数类型信息去动态创建委托类型。

  委托类型其实是一个数据结构,它引用静态方法或引用类实例及该类的实例方法。在我们定义一个委托类型时,C#会创建一个类,有点类似C++函数对象的概念,但是它们还是相差很远。我这里不再做太多说明,总之这个数据结构在lua侧是无法用类似CS.XXX去访问到的,正因为如此,所以才为什么所有的委托类型都需要打上[CSharpCallLua]标签去做一个映射表。lua不能访问到cs委托类型,没关系,我们可以在cs侧创建出来就行了。而Delegate 类是委托类型的基类,所有的泛型委托类型都可通过它进行函数调用的参数传递,现在先看下怎么样在lua怎么去用这个消息系统,然后再从上层到下层做一次分析:

 

 1 -- HotFix.UIArenaTest.lua
 2 -- Lua消息响应
 3 local TestLuaCallback = function(self, param)
 4     print('LuaDelegateTest: ', self, param, param and param.rank)
 5 end
 6 
 7 local TestLuaCallback2 = function(self, param)
 8     print('LuaDelegateTest: ', self, param, param and param.Count)
 9 end
10 
11 -- 添加消息示例
12 xlua.hotfix(CS.UIArena, 'AddListener', function(self)
13     ---------------------------------消息系统热更测试---------------------------------
14     -- 用法一:使用cs侧函数作为回调,必须在XLuaMessenger导出,无法新增消息监听,不支持重载函数
15     messenger.add_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, self.UpdatePanelInfo)
16     
17     -- 用法二:使用lua函数作为回调,必须在XLuaMessenger导出,可以新增任意已导出的消息监听
18     messenger.add_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, TestLuaCallback)
19     
20     -- 用法三:使用CS侧成员委托,无须在XLuaMessenger导出,可以新增同类型的消息监听,CS侧必须缓存委托
21     messenger.add_listener(CS.MessageName.MN_ARENA_UPDATE, self.updateLeftTimes)
22     
23     -- 用法四:使用反射创建委托,无须在XLuaMessenger导出,CS侧无须缓存委托,灵活度高,效率低,支持重载函数
24     -- 注意:如果该消息在CS代码中没有使用过,则最好打[ReflectionUse]标签,防止IOS代码裁剪
25     messenger.add_listener(CS.MessageName.MN_ARENA_BOX, self, 'SetBoxState', typeof(CS.System.Int32))
26 end)
27 
28 -- 移除消息示例
29 xlua.hotfix(CS.UIArena, 'RemoveListener', function(self)
30     -- 用法一
31     messenger.remove_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, self.UpdatePanelInfo)
32     
33     -- 用法二
34     messenger.remove_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, TestLuaCallback)
35     
36     -- 用法三
37     messenger.remove_listener(CS.MessageName.MN_ARENA_UPDATE, self.updateLeftTimes)
38     
39     -- 用法四
40     messenger.remove_listener(CS.MessageName.MN_ARENA_BOX, self, 'SetBoxState', typeof(CS.System.Int32))
41 end)
42 
43 -- 发送消息示例
44 util.hotfix_ex(CS.UIArena, 'OnGUI', function(self)
45     if Button(Rect(100, 300, 150, 80), 'lua BroadcastMsg1') then
46         local testData = CS.ArenaPanelData()--正确
47         --local testData = helper.new_object(typeof(CS.ArenaPanelData))--正确
48         testData.rank = 7777;
49         messenger.broadcast(CS.MessageName.MN_ARENA_PERSONAL_PANEL, testData)
50     end
51     
52     if Button(Rect(100, 400, 150, 80), 'lua BroadcastMsg3') then
53         local testData = CS.ArenaPanelData()
54         testData.rank = 7777;
55         messenger.broadcast(CS.MessageName.MN_ARENA_UPDATE, testData)
56     end
57 
58     if Button(Rect(100, 500, 150, 80), 'lua BroadcastMsg4') then
59         messenger.broadcast(CS.MessageName.MN_ARENA_BOX, 3)
60     end
61     self:OnGUI()
62 end)

 

  从lua侧逻辑层来说,有4种使用方式:

  1)使用cs侧函数作为回调:直接使用cs侧的函数作为回调,传递self.xxx函数接口,必须在XLuaMessenger导出,无法新增消息监听,不支持重载函数,XLuaMessenger稍后再做说明

  2)使用lua函数作为回调:在lua侧定义函数作为消息回调,必须在XLuaMessenger导出,可以新增任意已导出的消息监听

  3)使用CS侧成员委托:无须在XLuaMessenger导出,可以新增同类型的消息监听,CS侧必须缓存委托,这个之前也说了,委托作为类成员变量缓存,很方便在lua中使用

  4)使用反射创建委托:就是根据参数类型动态生成委托类型,无须在XLuaMessenger导出,CS侧无须缓存委托,灵活度高,效率低,支持重载函数。需要注意的是该委托类型必须没有被裁剪

  从以上4种使用方式来看,lua层逻辑代码使用消息系统十分简单,切灵活性很大,总有一种方式是可以实现消息系统热修复的。lua侧的整套消息系统用common.messenger.lua辅助实现,看下代码:

 

  1 -- common.messenger.lua
  2 -- added by wsh @ 2017-09-07 for Messenger-System-Proxy
  3 -- lua侧消息系统,基于CS.XLuaMessenger导出类,可以看做是对CS.Messenger的扩展,使其支持Lua
  4 
  5 local unpack = unpack or table.unpack
  6 local util = require 'common.util'
  7 local helper = require 'common.helper'
  8 local cache = {}
  9 
 10 local GetKey = function(...)
 11     local params = {...}
 12     local key = ''
 13     for _,v in ipairs(params) do
 14         key = key..'\t'..tostring(v)
 15     end
 16     return key
 17 end
 18 
 19 local GetCache = function(key)
 20     return cache[key]
 21 end
 22 
 23 local SetCache = function(key, value)
 24     assert(GetCache(key) == nil, 'already contains key '..key)
 25     cache[key] = value
 26 end
 27 
 28 local ClearCache = function(key)
 29     cache[key] = nil
 30 end
 31 
 32 local add_listener_with_delegate = function(messengerName, cs_del_obj)
 33     CS.XLuaMessenger.AddListener(messengerName, cs_del_obj)
 34 end
 35 
 36 local add_listener_with_func = function(messengerName, cs_obj, func)
 37     local key = GetKey(cs_obj, func)
 38     local obj_bind_callback = GetCache(key)
 39     if obj_bind_callback == nil then
 40         obj_bind_callback = util.bind(func, cs_obj)
 41         SetCache(key, obj_bind_callback)
 42         
 43         local lua_callback = CS.XLuaMessenger.CreateDelegate(messengerName, obj_bind_callback)
 44         CS.XLuaMessenger.AddListener(messengerName, lua_callback)
 45     end
 46 end
 47 
 48 local add_listener_with_reflection = function(messengerName, cs_obj, method_name, ...)
 49     local cs_del_obj = helper.new_callback(cs_obj, method_name, ...)
 50     CS.XLuaMessenger.AddListener(messengerName, cs_del_obj)
 51 end
 52 
 53 local add_listener = function(messengerName, ...)
 54     local params = {...}
 55     assert(#params >= 1, 'error params count!')
 56     if #params == 1 then
 57         add_listener_with_delegate(messengerName, unpack(params))
 58     elseif #params == 2 and type(params[2]) == 'function' then
 59         add_listener_with_func(messengerName, unpack(params))
 60     else
 61         add_listener_with_reflection(messengerName, unpack(params))
 62     end
 63 end
 64 
 65 local broadcast = function(messengerName, ...)
 66     CS.XLuaMessenger.Broadcast(messengerName, ...)
 67 end
 68 
 69 local remove_listener_with_delegate = function(messengerName, cs_del_obj)
 70     CS.XLuaMessenger.RemoveListener(messengerName, cs_del_obj)
 71 end
 72 
 73 local remove_listener_with_func = function(messengerName, cs_obj, func)
 74     local key = GetKey(cs_obj, func)
 75     local obj_bind_callback = GetCache(key)
 76     if obj_bind_callback ~= nil then
 77         ClearCache(key)
 78         
 79         local lua_callback = CS.XLuaMessenger.CreateDelegate(messengerName, obj_bind_callback)
 80         CS.XLuaMessenger.RemoveListener(messengerName, lua_callback)
 81     end
 82 end
 83 
 84 local remove_listener_with_reflection = function(messengerName, cs_obj, method_name, ...)
 85     local cs_del_obj = helper.new_callback(cs_obj, method_name, ...)
 86     CS.XLuaMessenger.RemoveListener(messengerName, cs_del_obj)
 87 end
 88 
 89 local remove_listener = function(messengerName, ...)
 90     local params = {...}
 91     assert(#params >= 1, 'error params count!')
 92     if #params == 1 then
 93         remove_listener_with_delegate(messengerName, unpack(params))
 94     elseif #params == 2 and type(params[2]) == 'function' then
 95         remove_listener_with_func(messengerName, unpack(params))
 96     else
 97         remove_listener_with_reflection(messengerName, unpack(params))
 98     end
 99 end
100 
101 return {
102     add_listener = add_listener,
103     broadcast = broadcast,
104     remove_listener = remove_listener,
105 }

 

  有以下几点需要说明:

  1)各个接口内部实现通过参数个数和参数类型实现重载,以下只对add_listener系列接口给出说明

  2)add_listener_with_delegate接受的参数直接是一个cs侧的委托对象,在lua侧不做任何特殊处理。对应上述的使用方式三

  3)add_listener_with_func接受参数是一个cs侧的对象,和一个函数,内部使用这两个信息创建闭包,传递给cs侧的是一个LuaFunction作为函数回调。对应上述的使用方式一和使用方式二

  4)add_listener_with_reflection接受的是一个cs侧的对象,外加一个cs侧的函数,或者是函数的名字和参数列表。对应的是使用方式四

  初步可以看出来,add_listener_with_delegate最简单,cs侧应该也不用做太多事情;add_listener_with_func不管是lua函数还是cs侧的函数,都是通过创建闭包,再将闭包函数映射到cs侧委托类型来创建的委托;add_listener_with_reflection看起来是通过反射动态创建的委托。所有接口的共通点就是想办法去创建委托,只是来源不一样。下面着重看下后两种方式是怎么实现的。

  对于反射创建委托,相对来说要简单一点,helper.new_callback最终会调用到XLuaHelper.cs中去,相关代码如下:

 

 1 // cs.XLuaHelper
 2 // 说明:创建委托
 3 // 注意:重载函数的定义顺序很重要:从更具体类型(Type)到不具体类型(object),xlua生成导出代码和lua侧函数调用匹配时都是从上到下的,如果不具体类型(object)写在上面,则永远也匹配不到更具体类型(Type)的重载函数,很坑爹
 4 public static Delegate CreateActionDelegate(Type type, string methodName, params Type[] paramTypes)
 5 {
 6     return InnerCreateDelegate(MakeGenericActionType, null, type, methodName, paramTypes);
 7 }
 8 
 9 public static Delegate CreateActionDelegate(object target, string methodName, params Type[] paramTypes)
10 {
11     return InnerCreateDelegate(MakeGenericActionType, target, null, methodName, paramTypes);
12 }
13 
14 public static Delegate CreateCallbackDelegate(Type type, string methodName, params Type[] paramTypes)
15 {
16     return InnerCreateDelegate(MakeGenericCallbackType, null, type, methodName, paramTypes);
17 }
18 
19 public static Delegate CreateCallbackDelegate(object target, string methodName, params Type[] paramTypes)
20 {
21     return InnerCreateDelegate(MakeGenericCallbackType, target, null, methodName, paramTypes);
22 }
23 
24 delegate Type MakeGenericDelegateType(params Type[] paramTypes);
25 static Delegate InnerCreateDelegate(MakeGenericDelegateType del, object target, Type type, string methodName, params Type[] paramTypes)
26 {
27     if (target != null)
28     {
29         type = target.GetType();
30     }
31 
32     BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
33     MethodInfo methodInfo = (paramTypes == null || paramTypes.Length == 0) ? type.GetMethod(methodName, bindingFlags) : type.GetMethod(methodName, bindingFlags, null, paramTypes, null);
34     Type delegateType = del(paramTypes);
35     return Delegate.CreateDelegate(delegateType, target, methodInfo);
36 }

 

  这部分代码没什么特别需要说明的,就是利用反射创建委托类型,这部分代码xlua作者在lua代码中也有实现。接下来的是怎么利用LuaFunction去创建委托,看下XLuaMesseneger.cs中创建委托的代码:

 

 1 public static Dictionary<string, Type> MessageNameTypeMap = new Dictionary<string, Type>() {
 2     // UIArena测试模块
 3     { MessageName.MN_ARENA_PERSONAL_PANEL, typeof(Callback<ArenaPanelData>) },//导出测试
 4     { MessageName.MN_ARENA_UPDATE, typeof(Callback<ArenaPanelData>) },//缓存委托测试
 5     { MessageName.MN_ARENA_BOX, typeof(Callback<int>) },//反射测试
 6 };
 7 
 8 
 9 [LuaCallCSharp]
10 public static List<Type> LuaCallCSharp = new List<Type>() {
11     // XLuaMessenger
12     typeof(XLuaMessenger),
13     typeof(MessageName),
14 };
15 
16 [CSharpCallLua]
17 public static List<Type> CSharpCallLua1 = new List<Type>() {
18 };
19 
20 // 由映射表自动导出
21 [CSharpCallLua]
22 public static List<Type> CSharpCallLua2 = Enumerable.Where(MessageNameTypeMap.Values, type => typeof(Delegate).IsAssignableFrom(type)).ToList();
23 
24 public static Delegate CreateDelegate(string eventType, LuaFunction func)
25 {
26     if (!MessageNameTypeMap.ContainsKey(eventType))
27     {
28         Debug.LogError(string.Format("You should register eventType : {0} first!", eventType));
29         return null;
30     }
31     return func.Cast(MessageNameTypeMap[eventType]);
32 }

 

  可以看到,这里用消息类型(String)和消息对应的委托类型做了一次表映射,lua侧传递LuaFunction过来时,通过消息类型就可以知道要Cast到什么类型的委托上面。由于时间关系(现在已经半夜4点半了),我这里也不再多说了,想要做更多了解的下载工程来看下源码就知道了,特别是这个Cast是干了什么,后面会贴身GitHub地址。这里多啰嗦一句,我这里是用消息类型对LuaFunction映射到委托进行了查表,而xlua中的原理是你所有导出的委托类型为一个列表,当LuaFunction要映射到委托类型时,会遍历这种表找一个参数类型匹配的委托执行映射。

  其它的应该都比较简单了,XLuaMessenger.cs是对Messenger.cs做了扩展,使其支持object类型参数,主要是提供对Lua侧发送消息的支持,截取其中一个函数来做下展示,这里也不再做过多说明:

 

 1 public static void Broadcast(string eventType, object arg1, object arg2)
 2 {
 3     Messenger.OnBroadcasting(eventType);
 4 
 5     Delegate d;
 6     if (Messenger.eventTable.TryGetValue(eventType, out d))
 7     {
 8         try
 9         {
10             Type[] paramArr = d.GetType().GetGenericArguments();
11             object param1 = arg1;
12             object param2 = arg2;
13             if (paramArr.Length >= 2)
14             {
15                 param1 = CastType(paramArr[0], arg1) ?? arg1;
16                 param2 = CastType(paramArr[1], arg2) ?? arg2;
17             }
18             d.DynamicInvoke(param1, param2);
19         }
20         catch (System.Exception ex)
21         {
22             Debug.LogError(string.Format("{0}:{1}", ex.Message, string.Format("arg1 = {0}, typeof(arg1) = {1}, arg2 = {2}, typeof(arg2) = {3}", arg1, arg1.GetType(), arg2, arg2.GetType())));
23             throw Messenger.CreateBroadcastSignatureException(eventType);
24         }
25     }
26 }

 

xlua动态库构建

  有关这个所谓的xlua轻量级热修复框架中的重点就这些,消息系统可以很好的说明,当xlua对你项目中的某些模块支持得很乏力的时候,你应该怎样去扩展xlua的热修复框架,使它支持对你项目代码的热修复。我对模块间的封装和解耦一直比较重视,从这个框架结构也可以看出来,各个模块的职责很清晰,也不做多余的事情。对消息系统的扩充也没有在原来的Messenger.cs做修改,而是另启了一个XLuaMessenger.cs脚本来实现。说句废话,一个模块或者一个系统的封装性好不好,或者说和其它模块的耦合度低不低,你想一个事情就可以了:假如,我要把这个系统、或者模块迁移到其他项目,我要做多少事情,怎样设计,可以让迁移变得方便。在设计模块的时候,你就应该考虑这个事情,由于最近我经常在整合和迁移游戏框架,导致我对这方面的设计敏感度特别高,如果当初就项目的某个模块设计不合理,封装性不好,进行项目迁移的时候真叫一个想骂娘,我脾气已经很好了,可真是脾气好的人都会吐血,切记。

  说到这差不多该说的都已经说了,之前看xlua讨论群里还有人问怎么构建xlua动态库,或者说怎么集成第三方插件。其实这个东西要说的真不多,懂点套路就行,关于xlua构建,我这里也不专门做讲解了,有兴趣的参考我的另一篇博客:Unity3D跨平台动态库编译—记kcp基于CMake的各平台构建实践。这里有kcp的构建,其实这是我第一次尝试去编译Unity各平台的动态库经历,整个构建都是参考的xlua构建,你看懂并实践成功了kcp的构建,那么xlua的也会了,没毛病。

 

工程项目地址

  github地址在:https://github.com/smilehao/xlua-framework

  不说了,很久没有码这么多字了,我一向是建议别人自己去读代码的,多读读优秀的代码才能切实进步。就像你不如坑,不知道水深一样;你不去读代码,只看别人理论分析还是有所欠缺的。

 

版权声明:本文为SChivas原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:http://www.cnblogs.com/SChivas/p/7893048.html