【基于tolua】C# 和 Lua 方法互调细节和互相持有引用问题

椎名林檎

tolua 是比较普遍的一个 Unity + Lua 开发的解决方案,本文记录使用 tolua 过程中的一些技术细节

1. C# 方法和变量如何导出供 lua 调用

在 tolua 框架下,如果你需要把你的 C# 类导出到 Lua ,你需要在 CustomSettings.cs 中用方法 _GT 把类名列添加到静态变量 customDelegateList中,例如导出 UnityEngine.GameObject

_GT(typeof(GameObject));

导出时,ToLuaExport会处理这个列表,自动生成对应的包装类 Wrap 文件UnityEngine_GameObjectWrap.cs,针对 GameObject 类中所有的方法、变量和属性,UnityEngine_GameObjectWrap.cs 文件中会自动生成对应的方法或 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 中创建类对应的 table 和元表,并将对应的 table 加入到 loaded 中,并设置类的通用方法 __gc, __index, __call
  • RegFunction 将成员函数转换为函数指针,添加到类的元表中
  • RegVar 为成员变量添加 getter 和 setter 方法,并转换为函数指针,添加到类元表中
  • EndClass 为 table 设置包含上述方法的元表

那么,Wrap 类的Register方法在什么时机被调用呢?当你启动 Lua 虚拟机时,使用 LuaBinder来绑定虚拟机,LuaBinderBind方法将执行虚拟机和 Wrap 类的绑定逻辑

2. lua 调用 C# 方法的全过程

2.1 在 lua 中实例化 C# 对象

在 lua 中的代码

local go = UnityEngine.GameObject("temp");

执行的流程大概是这样

  • Lua侧查找新建方法的函数指针
    在 lua 中的 GameObject 表中查找 New 方法(通过 Wrap 的 Register方法导出到 Lua的,看下面的代码),找不到于是在它的元表的 __index 中查找,找到了之前导出的函数指针
L.RegFunction("New", _CreateUnityEngine_GameObject);
  • Lua侧调用参数压栈
    将 lua 字符串 "temp" 压栈,同时将参数个数1压栈
  • C#侧取出参数并实例化
    根据函数指针调用到了 UnityEngine_GameObjectWrap类的CreateUnityEngine_GameObject方法,该方法中核心的代码如下,逻辑是从 Lua 栈中Pop出参数个数,然后从栈中Pop出字符串 "temp",然后调用 C# 的相关方法创建实例
            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有可能会申请内存空间,存在 GCAlloc,尽量少在 Lua 和 C# 之间传递字符串。

  • C# 侧包装实例对象并压栈
    查看上面的代码 ToLua.PushSealed(L, obj) 实现可以知道,实例实际上是被存在了 ObjectTranslator中维护的一个对象池 objects 中, 然后新建一个 userdata 类型的数据进行压栈
        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 侧从栈中获得对象引用
    lua 这边的变量 go 是一个 userdata 类型的变量,是对 C# 实例的引用
2.2 调用方法

接上面,lua 中的代码

go.transform.name = "abc";

执行的流程

  • 获取 get_transform 函数指针并将参数入栈
    GameObject 的元表中查找 get_transform 函数的指针,并将引用 go 入栈

  • C# 侧取出引用并调用对应的方法
    C# 这边执行 get_transform方法,从栈中取出userdata类型的引用数据,然后从 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# 拿到实例后,通过 transform属性得到返回值,同样缓存再 ObjectTranslator 对象列表中,同时生成一个 userdata 引用,压栈
  • Lua 侧从栈中取出引用
    Lua 侧从栈中取出实例的引用
  • 后续使用这个引用再调用 .name = "abc" 的方法如出一辙

这里可以看出来,Lua 调用 C# 方法的过程中,多次入栈出栈的操作和大量的类型转换,并伴随有引用数据的生成,甚至可能有临时对象的分配

3. C# 调用 lua 方法全过程

首先明确一点,C# 调用 Lua 方法,与 Wrap 类无关
下面是一段 C# 调用 lua 方法的代码,可以看出大概的流程

        LuaManager.Instance.OpenState();
        LuaTable luaTable = LuaManager.Instance.lua.DoFile<LuaTable>("SceneManager/login_scene_manager");
        LuaFunction func = luaTable.GetLuaFunction("Awake");
        func.Call();
        func.Dispose();
  • 调用时通过 DoStringDoFile 方法加载 lua 代码
  • 上述两个方法通过 laodBuffer 加载代码到 lua 虚拟机,得到 LuaTable 对象
  • 通过 GetFunction 获得对应的函数指针 LuaFunction 对象
  • 执行调用,调用的过程也同样涉及参数的压栈操作
  • 调用完成后将 LuaFunction 对象析构掉

如果需要获取返回值的情况,可以看看如下代码:

            LuaState luaMgr = LuaManager.Instance.lua;
            luaMgr.DoFile("Config/surveySetting.lua");
            LuaTable table = luaMgr.GetTable("SurveySettingConfig");
            LuaDictTable dict = table.ToDictTable();
            table.Dispose();

注意,C# 侧持有的 LuaTable 本质上也是一个 lua 对象的引用,需要调用 table.Dispose() 来解引用

4. lua 和 C# 互相持有引用情况分析

lua 通常是用来做UI界面开发的,我们在开发的过程中进行界面管理,往往会存在如下这种情况


对象相互持有
  • 在 C# 这边持有 lua 的 UI 界面对应的 table 引用
  • Lua 侧 UI 界面 table 中有各种 UI 控件成员,实际上是 userdata 的引用,这些控件的实例存放在 C# 测的对象池中,甚至有可能有 lua 方法被注册到 C# 这边的按钮实例中
    存在问题:
    当关闭 UI 界面时,C# 未将持有的 panel 引用析构,未将注册的回调方法注销,则会导致双方互相持有引用,GC 时对象无法回收
    避免出现这种问题,需要确保
  • 界面关闭时,将 Lua 侧的 ui table 对象要被置为 nil 且不要被引用,(button\image\label 等成员可以不用置为 nil,因为GC可达性检查时唯一能到根对象的 ui table 无人引用)
  • C# 侧析构对界面的引用 panel,调用 panel.Dispose();
  • Button/Image/Label 这些C#测的对象,在 C# GC 时会被回收掉
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容