Unity3D遊戲輕量級xlua熱修復框架

一  這是什麼東西

  前陣子剛剛集成xlua到項目,目的只有一個:對線上遊戲C#邏輯有Bug的地方執行修復,經過考察xlua和tolua,最終選擇了xlua,很大部分緣由是由於項目已經到了後期,線上版本迭代了好幾回,因此引入Lua的目的不是爲了開發新版本模塊。xlua在咱們的這種狀況下非常適用,如xlua做者所說,用C#開發,用lua熱更,xlua這套框架爲咱們提供了諸多便利,至少我能夠說,在面臨一樣的狀況下,你用tolua去作一樣的事情是很費心的。可是若是你是想用xlua作整套客戶端遊戲邏輯的,這篇文對你可能就沒什麼借鑑意義了。其實純lua寫邏輯,使用xlua仍是tolua並非那麼重要,由於與c#交互會少不少,並且通常都是耗性能的地方纔放c#,即便網上有各類lua框架性能的評測,其實我感受意義都不太大,若是真要頻繁調用,那無論xlua仍是tolua你都要考慮方案去優化的。php

  當時在作完這個xlua熱更框架,本打算寫篇博文分享一下。後來,因爲工做一直比較忙,這個事情就被擱淺了下來,另外,集成xlua時本身寫的代碼少得可伶,感受也沒什麼太多要分享的地方。畢竟熱修復,本質上來講就是一個輕量級的東西。除非你是新開的項目,一開始就遵循xlua熱更的各類規範。而若是你是後期引入的xlua,那麼,xlua熱修復代碼的複雜度,很大程度上取決於你框架原先c#代碼的寫法,好比說委託的使用,在c#側常常做爲回調去使用,xlua的demo裏對委託的熱修復示例是這樣的:html

 

 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就能訪問,很簡單。而問題就在於,咱們項目以前對委託的使用方式是這樣的:git

 

 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熱更規則的相關代碼規範,從而修復困難。github

  這個例子可能舉得不是太好,你能夠暴力修改項目中全部這樣寫法的地方(只要你樂意- -),另外,下面的這種寫法有GC問題,這個問題是項目歷史遺留下來的。編程

二  現行xlua分享的弊端

  當初在集成xlua到項目時,發現現行網絡上對xlua的大多分享,沒有直接命中我所面臨的問題,有實際借鑑意義的項目很少,對不少分享來講:c#

  1)體積過重:集成了各類資源熱更新、場景管理、音樂管理、定時器管理等等邊緣模塊,xlua內容反而顯得過輕。緩存

  2)拈輕怕重:簡單集成xlua,而後本身用NGUI或者UGUI寫了個小demo,完事。網絡

三  輕量級xlua熱修復框架

  其實說是xlua的一個擴展更加貼切,對xlua沒有提供的一些外圍功能進行了擴展。xlua的設計仍是挺不錯的,相比tolua的代碼讀起來仍是要清爽多了。數據結構

3.1  框架工程結構

  我假設你已經清楚了xlua作熱修復的基本流程,由於下面不會對xlua自己的熱更操做作太多說明。先一張本工程的截圖:閉包

 

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#腳本

  6)Resources/xlua/HotFix:熱修復腳本

  須要說明的一點是,這裏全部的熱修復示例我都沒有單獨去作demo演示了,其實若是你真的須要,本身去寫測試也沒多大問題,全部Lua熱更對應的C#邏輯都在,好進行對比。本文主要說的方向有這麼幾點:

  1)消息系統:打通cs和lua側的消息系統,其中的關鍵問題是泛型委託

  2)對象建立:怎麼樣在lua側建立cs對象,特別是泛型對象

  3)迭代器:cs側列表、字典之類的數據類型,怎樣在lua側泛型迭代

  4)協程:cs側協程怎麼熱更,怎麼在lua側建立協程

  5)委託做爲回調:cs側函數用做委託回調,看成函數調用的形參時,怎樣在lua側傳遞委託形參

3.2  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 }

3.3  lua側cs迭代器訪問

  xlua做者在demo中給出了示例,只是我的以爲用起來麻煩,因此包裝了一層語法糖,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使用迭代器的工做機制是有點雷同的,若是你清楚這個機制,那麼這裏的原理就不難理解。

3.4  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     --    一、若是使用cs側函數,必須將最後一個參數的回調(cs側定義爲委託)導出到[CSharpCallLua]
28     --    二、用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做者已經對協程作了比較好的支持,不須要咱們另外去操心太多。

3.5  lua側建立cs委託回調

  這裏迴歸的是篇頭所闡述的問題,當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 --    一、緩存委託
 4 --    二、Lua綁定(其實是建立LuaFunction再cast到delegate),須要在委託類型上打[CSharpCallLua]標籤--推薦
 5 --    三、使用反射再執行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         --    一、使用反射建立委託---這裏無法直接使用,返回的是Callback<,>類型,無法隱式轉換到CS.GameObjectPool.GetGameObjectDelegate類型
22         --    二、再執行Lua綁定--須要在委託類型上打[CSharpCallLua]標籤
23         -- 注意:
24         --    一、使用反射建立的委託能夠直接在Lua中調用,但做爲參數時,必需要求參數類型一致,或者參數類型爲Delegate--參考Lua側消息系統實現
25         --    二、正由於存在類型轉換問題,而CS側的委託類型在Lua中無法拿到,因此在Lua側執行類型轉換成爲了避免可能,上面才使用了Lua綁定
26         --    三、對於Lua側無法執行類型轉換的問題,能夠在CS側去作,這就是[CSharpCallLua]標籤的做用,xlua底層已經爲咱們作好這一步
27         --    四、因此,這裏至關於方式二多包裝了一層委託,從這裏能夠知道,委託作好所有打[CSharpCallLua]標籤,不然更新起來很受限
28         --    五、對於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做者建議將工程中全部的委託類型打上這個標籤。

  3)使用反射再執行lua綁定:這種方式使用起來很受限,這裏再也不作說明,要了解的朋友本身參考源代碼。

 

3.6  打通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最簡單;add_listener_with_func經過建立閉包,再將閉包函數映射到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到什麼類型的委託上面。而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的熱更真的和被熱更的cs項目有很大的直接牽連,仍是拿篇頭那個委託熱更的例子作下說明:若是你cs項目代碼規範就就已經支持了xlua熱更,那本文中不少關於委託熱更的討論你根本就用不上。可是這裏給的代碼組織結構和解決問題的思路仍是頗有參考性的,實踐時你項目中遇到某些難以熱更的模塊,能夠參考這裏消息系統的設計思路去解決。

  另外,以前看xlua討論羣裏還有人問怎麼構建xlua動態庫,或者怎麼集成第三方插件。這個問題能夠參考個人另外一篇博客:Unity3D跨平臺動態庫編譯---記kcp基於CMake的各平臺構建實踐。這裏有kcp的構建,其實這是我第一次嘗試去編譯Unity各平臺的動態庫經歷,整個構建都是參考的xlua構建工程,你看懂並實踐成功了kcp的構建,那麼xlua的也會了。

 

五  工程項目地址

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

相關文章
相關標籤/搜索