xLua的obj引用分析

8 篇文章 0 订阅
4 篇文章 1 订阅

xLua的obj引用分析

为了防止c#和lua两端的内存泄漏,有必要了解xLua是怎样处理2端的引用关系的,尤其是在扩展xLua时,处理不得当很容易造成引用丢失或者内存泄漏

一个c#的obj是不能直接传递到lua,需要一个中间层,这个中间层就是userdata。xLua会为每个传递到lua的obj生成唯一的一个userdata,并将2者绑定起来(具体绑定方式后面分析)。

这样就有2个基本问题:

  1. 双方查询
    • 可以通过obj查找到其对应的userdata(一般是在将obj压到lua时,要先查询其有没有对应的userdata,保证同一个obj对应同一个userdata
    • 可以通过userdate查找到其对应的obj(在wrap文件中大量使用,lua下通过userdata调用其c#层的函数,那c#层就需要通过这个userdata找回其obj,完成函数调用
  2. GC管理
    • obj在c#层被GC了,其对应的userdata也应该赋nil
    • userdata在lua层被GC了,清理其对应的obj的引用。

双方查询

考虑到lua无法直接保存c#对象的引用,所以一般的做法就是先在c#层为obj产生一个唯一id,然后和lua交互时都是直接传输的这个唯一id,但通过一个数字是无法模仿面向对象的使用方式的,最终做法就是通过将数字记录在userdata中,再设置userdata的元表来达到模拟面向对象的使用方式。

下面的代码显示了创建这个userdata的过程:

// 文件:xlua.c
static void cacheud(lua_State *L, int key, int cache_ref) {
    // 将userdata放入cache表中
    // 下面代码可以理解为:cache[key] = userdata
    lua_rawgeti(L, LUA_REGISTRYINDEX, cache_ref);
    lua_pushvalue(L, -2);
    lua_rawseti(L, -2, key);
    lua_pop(L, 1);
}

// 下面函数是第一次将csobj压入lua栈时调用,函数内部会先为csobj产生一个userdata
// 其中参数key就是csobj的唯一id
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
    // userdata就是一个大小为int的内存
    int* pointer = (int*)lua_newuserdata(L, sizeof(int));

    // 在userdata的内存里写入key
    *pointer = key;

    if (need_cache) cacheud(L, key, cache_ref);

    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);

    lua_setmetatable(L, -2);
}

还要注意到,上面代码还将userdata放入了cache表,而且key就是obj的唯一id,这是为了后续方便快速判定一个obj是否已经产生过userdata了。

在xLua中,基本是通过2个容器来管理obj,在ObjectTranslator中:

// 文件:ObjectTranslator.cs
public partial class ObjectTranslator
{
    // key是obj唯一id
    // value是obj
    internal readonly ObjectPool objects = new ObjectPool();
  
    // key是obj
    // value是obj唯一id
    internal readonly Dictionary<object, int> reverseMap = new Dictionary<object, int>(new ReferenceEqualsComparer());

    // ...
}

其中ObjectPool采用了一种叫FreeList的数据结构来保存userdata和obj,除了不方便遍历外,其他操作如:增、删、查询都是 O ( 1 ) O(1) O(1)的复杂度,这里可以简单认为其就是一个Dictionary<int, object>

下面代码显示了obj唯一id的产生过程。当一个obj第一次传递到lua之前,会调用ObjectTranslator.addObject函数来设置这objectsreverseMap这2个容器,并产生一个唯一id:

// 文件:ObjectTranslator.cs
int addObject(object obj, bool is_valuetype, bool is_enum)
{
    // 这个index可以理解为就是obj的唯一id(其实就是obj在ObjectPool的数组下标)
    // 后续可以通过objects.Get(index)查询到这个obj
    int index = objects.Add(obj);
    if (is_enum)
    {
        enumMap[obj] = index;
    }
    else if (!is_valuetype)
    {
        // 反向引用,后续会讲到
        reverseMap[obj] = index;
    }

    return index;
}

当obj真正被传到lua时,会调用到下面的ObjectTranslator.Push函数:

// 文件:ObjectTranslator.cs
public void Push(RealStatePtr L, object o)
{
    if (o == null)
    {
        LuaAPI.lua_pushnil(L);
        return;
    }

    int index = -1;
    Type type = o.GetType();
    bool is_enum = type.IsEnum;
    bool is_valuetype = type.IsValueType;
    bool needcache = !is_valuetype || is_enum;
    // 首先通过reverseMap找到obj的唯一id
    if (needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index)))
    {
        // 这时可以直接将cache里的userdata传给lua
        if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1)
        {
            return;
        }
    }

    bool is_first;
    int type_id = getTypeId(L, type, out is_first);

    //如果一个type的定义含本身静态readonly实例时,getTypeId会push一个实例,这时候应该用这个实例
    if (is_first && needcache && (is_enum ? enumMap.TryGetValue(o, out index) : reverseMap.TryGetValue(o, out index)))
    {
        if (LuaAPI.xlua_tryget_cachedud(L, index, cacheRef) == 1)
        {
            return;
        }
    }

    // 为obj产生唯一id
    index = addObject(o, is_valuetype, is_enum);
    // 为obj产生userdata,放入cache,再传给lua
    // 可以看到,第二个参数传的就是index,对应c代码里的key参数
    LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
}

此时,可以理解第1个查询问题,c#层可以通过reverseMap查询obj的唯一id,lua可以根据这个唯一id在cache表里找到对应的userdata了。

同时也可以理解第2个查询问题了,通过userdata可以获取到obj的唯一id,然后可以根据这个唯一id在objects中找到对应的obj,随便查看一个wrap文件,有如下类似代码:

// wrap文件,此时ud在栈的位置是1
object __cl_gen_to_be_invoked = translator.FastGetCSObj(L, 1);

// 文件:ObjectTranslator.cs
internal object FastGetCSObj(RealStatePtr L,int index)
{
    // xlua_tocsobj_fast函数会返回index位置的userdata里保存的值,也就是之前我们存入的obj唯一id
    return getCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index));
}

// 参数udata就是obj的唯一id了
private object getCsObj(RealStatePtr L, int index, int udata)
{
    object obj;
    if (udata == -1)
    {
        if (LuaAPI.lua_type(L, index) != LuaTypes.LUA_TUSERDATA) return null;

        Type type = GetTypeOf(L, index);
        if (type == typeof(decimal))
        {
            decimal v;
            Get(L, index, out v);
            return v;
        }
        GetCSObject get;
        if (type != null && custom_get_funcs.TryGetValue(type, out get))
        {
            return get(L, index);
        }
        else
        {
            return null;
        }
    }
    else if (objects.TryGetValue(udata, out obj)) // 这里通过udata查找obj并返回
    {
#if !UNITY_5 && !XLUA_GENERAL
        if (obj != null && obj is UnityEngine.Object && ((obj as UnityEngine.Object) == null))
        {
            throw new UnityEngine.MissingReferenceException("The object of type '"+ obj.GetType().Name +"' has been destroyed but you are still trying to access it.");
        }
#endif
        return obj;
    }
    return null;
}

可以看下xlua_tocsobj_fast的源码,实现也非常简单:

// 文件:xlua.c
LUA_API int xlua_tocsobj_fast (lua_State *L,int index) {
    int *udata = (int *)lua_touserdata (L,index);

    if(udata!=NULL) 
        return *udata;
    return -1;
}

可以发现,obj的唯一id在c#和lua之间扮演了很重要的中间层,2端都是通过这个唯一id来交互的。

这里整理一下这个过程:

  • 通过objects.Add来产生唯一id
  • 通过xlua_pushcsobj将id存放在userdata中
  • 通过xlua_tocsobj_fast获取到存放在userdata中的唯一id
  • 通过objects.Get来根据唯一id获取到obj

引用分析

首先看下userdata的引用。在交互过程中,会对userdata产生引用的地方就是在xlua_pushcsobj函数中会将userdata存放在cache表中,但仔细查看cache表的创建,可以发现这个表是个弱表(weak table),不会对userdata产生任何的引用。

具体可以看ObjectTranslator的构造函数:

public ObjectTranslator(LuaEnv luaenv,RealStatePtr L)
{
    // some code ...

    // 下面代码可以理解为:
    // cache = setmetatable({}, { __mode = 'v' })
    LuaAPI.lua_newtable(L);
    LuaAPI.lua_newtable(L);
    LuaAPI.xlua_pushasciistring(L, "__mode");
    LuaAPI.xlua_pushasciistring(L, "v");
    LuaAPI.lua_rawset(L, -3);
    LuaAPI.lua_setmetatable(L, -2);
    cacheRef = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);

    initCSharpCallLua();
}

然后看下obj的引用。在交互过程中,会对obj产生引用的地方是ObjectTranslator.addObject函数里,objectsreverseMap都对obj产生了引用。

GC分析

上面讲到在交互过程中没有产生对userdata的额外引用,所以一个userdata传递给lua后,如果在lua下没有地方引用这个userdata了,那么它就会被GC了。这一点非常重要,因为如果需要一个userdata的生命周期要跟随obj的生命周期的话,obj就必须要自己对这个userdata生成一个引用,否则这个userdata就有可能因为在lua下没人引用它而导致被GC。举个例子就是Peer机制,由于peer是绑在userdata上的,如果userdata被GC了,其peer也会丢失,这样下次obj被传递到lua时,就会生成一个新的userdata,从而丢失了其peer内容。

userdata的GC处理是通过__gc这个metamethod实现的(lua5.1只支持userdata的__gc,table都不支持),xLua会把所有userdata的metatable都设置一个统一的__gc函数,即StaticLuaCallbacks.LuaGC,看下面的代码:

// 文件:StaticLuaCallbacks.cs
[MonoPInvokeCallback(typeof(LuaCSFunction))]
public static int LuaGC(RealStatePtr L)
{
    try
    {
        // udata就是obj的唯一id
        int udata = LuaAPI.xlua_tocsobj_safe(L, 1);
        if (udata != -1)
        {
            ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
            translator.collectObject(udata);
        }
        return 0;
    }
    catch (Exception e)
    {
        return LuaAPI.luaL_error(L, "c# exception in LuaGC:" + e);
    }
}

// 文件:ObjectTranslator.cs
internal void collectObject(int obj_index_to_collect)
{
    object o;

    if (objects.TryGetValue(obj_index_to_collect, out o))
    {
        // 清理objects
        objects.Remove(obj_index_to_collect);

        if (o != null)
        {
            int obj_index;
            bool is_enum = o.GetType().IsEnum;
            if ((is_enum ? enumMap.TryGetValue(o, out obj_index) : reverseMap.TryGetValue(o, out obj_index))
                && obj_index == obj_index_to_collect)
            {
                if (is_enum)
                {
                    enumMap.Remove(o);
                }
                else
                {
                    // 清理reverseMap
                    reverseMap.Remove(o);
                }
            }
        }
    }
}

注意cache表不需要清理,因为它是弱表!

最后来看obj的GC处理。上面讲到objectsreveresMap都会对obj产生引用,也就是说这个obj就算c#层其他地方都不引用它了,它也不会被GC,直到lua层也没人引用obj对应的userdata了,userdata被GC导致objectsreverseMap被清理,obj才能被GC。所以,只要lua层还有地方引用这个obj(对应的userdata),那么这个obj就不会被GC,这是很重要的一点,为了防止内存泄漏,lua层的引用必须管理好,否则会导致c#层的内存泄漏

不过有一种情况,就算lua层还存在引用,obj仍然会被GC,这种情况就是这个obj是UnityEngine.Object派生的!由于可以通过GameObject.Destroy等函数显式删除一个UnityEngine.Object,此时c#层所有引用这个UnityEngine.Object的变量的值都会变成null。
这其实是Unity的一个trick,Unity重载了UnityEngine.Object==运算符,当一个UnityEngine.Object被Destroy后,通过==判断会发现其和null相等,但其实这个obj并没有真正被GC。
为了处理这种情况,xLua采用了一个GC轮询,当发现一个UnityEngine.Object被Destroy后,会对其userdata做一些清理。看下面的代码:

// LuaEnv.cs
#if !XLUA_GENERAL
int last_check_point = 0;

int max_check_per_tick = 20;

static bool ObjectValidCheck(object obj)
{
    // 如果obj是UnityEngine.Object而且==null,则任何其已经被Destroy了
    return (!(obj is UnityEngine.Object)) ||  ((obj as UnityEngine.Object) != null);
}

Func<object, bool> object_valid_checker = new Func<object, bool>(ObjectValidCheck);
#endif

public void Tick()
{
#if THREAD_SAFT || HOTFIX_ENABLE
    lock (luaEnvLock)
    {
#endif
        var _L = L;
        lock (refQueue)
        {
            while (refQueue.Count > 0)
            {
                GCAction gca = refQueue.Dequeue();
                translator.ReleaseLuaBase(_L, gca.Reference, gca.IsDelegate);
            }
        }
#if !XLUA_GENERAL
        last_check_point = translator.objects.Check(last_check_point, max_check_per_tick, object_valid_checker, translator.reverseMap);
#endif
#if THREAD_SAFT || HOTFIX_ENABLE
    }
#endif
}

public void GC()
{
    Tick();
}

可以看到,在Tick函数中,会把ObjectValidCheck函数传给objects,objects.Check函数内部会遍历指定数量的obj,通过ObjectValidCheck判断其是否已经失效(被Destroy)了,如果失效的话,对应的obj会被赋null。这样外部通过userdata查询到的obj就会是null。

要注意到,userdata自身不是nil,只是其对应的obj是null,所以在lua下通过判断变量是否为nil是不能发现unity obj被删除了,可以编写一个简单函数来判断一个unity obj对应的userdata是否失效了。

-- unity对象判断为空, 如果你有些对象是在c#删掉了,lua不知道
-- 判断这种对象为空时可以用下面这个函数。
function IsNil(uobj)
    return uobj == nil or uobj:Equals(nil)
end

最后的最后,分析一个容易出现的循环引用问题:

上面提到,如果obj需要保证userdata的生命周期,会存储一个userdata的引用,那这个引用应该什么时候释放呢?xLua里的LuaBase类也负责了lua引用的管理,它是采用Dispose和析构函数的方式来释放lua引用的。那我们也可以为obj实现Dispose和析构函数来释放引用吗?答案是不行的,这样会造成循环引用。由于obj自身被objectsreverseMap引用着,这个引用必须等到userdata被GC才会释放,而userdata却又被obj引用了,从而产生循环引用,所以obj根本不可能触发到析构函数,也无法释放userdata引用,最后造成内存泄漏。

怎么解决呢?可以通过提供一个统一的Destroy函数来做释放。所有Object不用时都需要调用这个函数来做清理工作,调用这个函数后,就可以认为其已经不能再使用了,这样就可以在这个Destroy函数里清理userdata的引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值