
tolua 是比較普遍的一個 Unity + Lua 開發(fā)的解決方案,本文記錄使用 tolua 過程中的一些技術(shù)細(xì)節(jié)
1. C# 方法和變量如何導(dǎo)出供 lua 調(diào)用
在 tolua 框架下,如果你需要把你的 C# 類導(dǎo)出到 Lua ,你需要在 CustomSettings.cs 中用方法 _GT 把類名列添加到靜態(tài)變量 customDelegateList中,例如導(dǎo)出 UnityEngine.GameObject
_GT(typeof(GameObject));
導(dǎo)出時,ToLuaExport會處理這個列表,自動生成對應(yīng)的包裝類 Wrap 文件UnityEngine_GameObjectWrap.cs,針對 GameObject 類中所有的方法、變量和屬性,UnityEngine_GameObjectWrap.cs 文件中會自動生成對應(yīng)的方法或 getter 和 setter 方法,另外額外生成一個 Register方法。
所有 Wrap 類中, Register 方法的組成都相對固定,比如
public static void Register(LuaState L)
{
L.BeginClass(typeof(StoredBuddy), typeof(System.Object));
L.RegFunction("New", _CreateStoredBuddy);
L.RegFunction("GetBuddyLevel", GetBuddyLevel);
L.RegFunction("__tostring", ToLua.op_ToString);
L.RegVar("bid", get_bid, set_bid);
L.RegVar("exp", get_exp, set_exp);
L.RegVar("level", get_level, set_level);
L.EndClass();
}
-
BeginClass在 Lua 中創(chuàng)建類對應(yīng)的 table 和元表,并將對應(yīng)的 table 加入到 loaded 中,并設(shè)置類的通用方法__gc,__index,__call等 -
RegFunction將成員函數(shù)轉(zhuǎn)換為函數(shù)指針,添加到類的元表中 -
RegVar為成員變量添加 getter 和 setter 方法,并轉(zhuǎn)換為函數(shù)指針,添加到類元表中 -
EndClass為 table 設(shè)置包含上述方法的元表
那么,Wrap 類的Register方法在什么時機(jī)被調(diào)用呢?當(dāng)你啟動 Lua 虛擬機(jī)時,使用 LuaBinder來綁定虛擬機(jī),LuaBinder的Bind方法將執(zhí)行虛擬機(jī)和 Wrap 類的綁定邏輯
2. lua 調(diào)用 C# 方法的全過程
2.1 在 lua 中實(shí)例化 C# 對象
在 lua 中的代碼
local go = UnityEngine.GameObject("temp");
執(zhí)行的流程大概是這樣
- Lua側(cè)查找新建方法的函數(shù)指針
在 lua 中的 GameObject 表中查找New方法(通過 Wrap 的Register方法導(dǎo)出到 Lua的,看下面的代碼),找不到于是在它的元表的__index中查找,找到了之前導(dǎo)出的函數(shù)指針
L.RegFunction("New", _CreateUnityEngine_GameObject);
- Lua側(cè)調(diào)用參數(shù)壓棧
將 lua 字符串 "temp" 壓棧,同時將參數(shù)個數(shù)1壓棧 - C#側(cè)取出參數(shù)并實(shí)例化
根據(jù)函數(shù)指針調(diào)用到了UnityEngine_GameObjectWrap類的CreateUnityEngine_GameObject方法,該方法中核心的代碼如下,邏輯是從 Lua 棧中Pop出參數(shù)個數(shù),然后從棧中Pop出字符串 "temp",然后調(diào)用 C# 的相關(guān)方法創(chuàng)建實(shí)例
int count = LuaDLL.lua_gettop(L);
if (count == 1 && TypeChecker.CheckTypes<string>(L, 1))
{
string arg0 = ToLua.ToString(L, 1);
UnityEngine.GameObject obj = new UnityEngine.GameObject(arg0);
ToLua.PushSealed(L, obj);
return 1;
}
注意,這里的
ToLua.ToString有可能會申請內(nèi)存空間,存在 GCAlloc,盡量少在 Lua 和 C# 之間傳遞字符串。
- C# 側(cè)包裝實(shí)例對象并壓棧
查看上面的代碼ToLua.PushSealed(L, obj)實(shí)現(xiàn)可以知道,實(shí)例實(shí)際上是被存在了ObjectTranslator中維護(hù)的一個對象池objects中, 然后新建一個userdata類型的數(shù)據(jù)進(jìn)行壓棧
public static void PushUserData(IntPtr L, object o, int reference)
{
int index;
ObjectTranslator translator = ObjectTranslator.Get(L);
if (translator.Getudata(o, out index))
{
if (LuaDLL.tolua_pushudata(L, index))
{
return;
}
translator.Destroyudata(index);
}
index = translator.AddObject(o);
LuaDLL.tolua_pushnewudata(L, reference, index);
}
- lua 側(cè)從棧中獲得對象引用
lua 這邊的變量 go 是一個userdata類型的變量,是對 C# 實(shí)例的引用
2.2 調(diào)用方法
接上面,lua 中的代碼
go.transform.name = "abc";
執(zhí)行的流程
獲取
get_transform函數(shù)指針并將參數(shù)入棧
從GameObject的元表中查找get_transform函數(shù)的指針,并將引用go入棧C# 側(cè)取出引用并調(diào)用對應(yīng)的方法
C# 這邊執(zhí)行get_transform方法,從棧中取出userdata類型的引用數(shù)據(jù),然后從ObjectTranslator的對象池列表中取出C#對象
public static object ToObject(IntPtr L, int stackPos)
{
int udata = LuaDLL.tolua_rawnetobj(L, stackPos);
if (udata != -1)
{
ObjectTranslator translator = ObjectTranslator.Get(L);
return translator.GetObject(udata);
}
return null;
}
- C# 側(cè)調(diào)用實(shí)例方法,將返回值壓棧
C# 拿到實(shí)例后,通過transform屬性得到返回值,同樣緩存再ObjectTranslator對象列表中,同時生成一個userdata引用,壓棧 - Lua 側(cè)從棧中取出引用
Lua 側(cè)從棧中取出實(shí)例的引用 - 后續(xù)使用這個引用再調(diào)用 .name = "abc" 的方法如出一轍
這里可以看出來,Lua 調(diào)用 C# 方法的過程中,多次入棧出棧的操作和大量的類型轉(zhuǎn)換,并伴隨有引用數(shù)據(jù)的生成,甚至可能有臨時對象的分配
3. C# 調(diào)用 lua 方法全過程
首先明確一點(diǎn),C# 調(diào)用 Lua 方法,與 Wrap 類無關(guān),
下面是一段 C# 調(diào)用 lua 方法的代碼,可以看出大概的流程
LuaManager.Instance.OpenState();
LuaTable luaTable = LuaManager.Instance.lua.DoFile<LuaTable>("SceneManager/login_scene_manager");
LuaFunction func = luaTable.GetLuaFunction("Awake");
func.Call();
func.Dispose();
- 調(diào)用時通過
DoString或DoFile方法加載 lua 代碼 - 上述兩個方法通過
laodBuffer加載代碼到 lua 虛擬機(jī),得到LuaTable對象 - 通過
GetFunction獲得對應(yīng)的函數(shù)指針LuaFunction對象 - 執(zhí)行調(diào)用,調(diào)用的過程也同樣涉及參數(shù)的壓棧操作
- 調(diào)用完成后將
LuaFunction對象析構(gòu)掉
如果需要獲取返回值的情況,可以看看如下代碼:
LuaState luaMgr = LuaManager.Instance.lua;
luaMgr.DoFile("Config/surveySetting.lua");
LuaTable table = luaMgr.GetTable("SurveySettingConfig");
LuaDictTable dict = table.ToDictTable();
table.Dispose();
注意,C# 側(cè)持有的 LuaTable 本質(zhì)上也是一個 lua 對象的引用,需要調(diào)用
table.Dispose()來解引用
4. lua 和 C# 互相持有引用情況分析
lua 通常是用來做UI界面開發(fā)的,我們在開發(fā)的過程中進(jìn)行界面管理,往往會存在如下這種情況

- 在 C# 這邊持有 lua 的 UI 界面對應(yīng)的 table 引用
- Lua 側(cè) UI 界面 table 中有各種 UI 控件成員,實(shí)際上是
userdata的引用,這些控件的實(shí)例存放在 C# 測的對象池中,甚至有可能有 lua 方法被注冊到 C# 這邊的按鈕實(shí)例中
存在問題:
當(dāng)關(guān)閉 UI 界面時,C# 未將持有的 panel 引用析構(gòu),未將注冊的回調(diào)方法注銷,則會導(dǎo)致雙方互相持有引用,GC 時對象無法回收
避免出現(xiàn)這種問題,需要確保 - 界面關(guān)閉時,將 Lua 側(cè)的 ui table 對象要被置為 nil 且不要被引用,(button\image\label 等成員可以不用置為 nil,因?yàn)镚C可達(dá)性檢查時唯一能到根對象的 ui table 無人引用)
- C# 側(cè)析構(gòu)對界面的引用 panel,調(diào)用
panel.Dispose(); - Button/Image/Label 這些C#測的對象,在 C# GC 時會被回收掉