【基于tolua】C# 和 Lua 方法互調(diào)細(xì)節(jié)和互相持有引用問題

椎名林檎

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ī),LuaBinderBind方法將執(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)用時通過 DoStringDoFile 方法加載 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 時會被回收掉
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容