找回密码
 立即注册
查看: 248|回复: 0

xLua的obj引用分析

[复制链接]
发表于 2021-8-13 17:42 | 显示全部楼层 |阅读模式
xLua的obj引用分析

为了防止c#和lua两端的内存泄漏,有必要了解xLua是怎样处理2端的引用关系的,尤其是在扩展xLua时,处理不得当很容易造成引用丢失或者内存泄漏
一个c#的obj是不能直接传递到lua,需要一个中间层,这个中间层就是userdata。xLua会为每个传递到lua的obj生成唯一的一个userdata,并将2者绑定起来(具体绑定方式后面分析)。
这样就有2个基本问题:

  • 双方查询
      可以通过obj查找到其对应的userdata(一般是在将obj压到lua时,要先查询其有没有对应的userdata,保证同一个obj对应同一个userdata)可以通过userdate查找到其对应的obj(在wrap文件中大量使用,lua下通过userdata调用其c#层的函数,那c#层就需要通过这个userdata找回其obj,完成函数调用

  • GC管理
      obj在c#层被GC了,其对应的userdata也应该赋nil。userdata在lua层被GC了,清理其对应的obj的引用。

双方查询

考虑到lua无法直接保存c#对象的引用,所以一般的做法就是先在c#层为obj产生一个唯一id,然后和lua交互时都是直接传输的这个唯一id,但通过一个数字是无法模仿面向对象的使用方式的,最终做法就是通过将数字记录在userdata中,再设置userdata的元表来达到模拟面向对象的使用方式。
下面的代码显示了创建这个userdata的过程:
  1. // 文件:xlua.cstaticvoidcacheud(lua_State *L,int key,int cache_ref){// 将userdata放入cache表中// 下面代码可以理解为:cache[key] = userdatalua_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
  2. LUA_API voidxlua_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中:
  1. // 文件:ObjectTranslator.cspublicpartialclassObjectTranslator{// key是obj唯一id// value是objinternalreadonlyObjectPool objects =newObjectPool();// key是obj// value是obj唯一idinternalreadonly Dictionary<object,int> reverseMap =newDictionary<object,int>(newReferenceEqualsComparer());// ...}
复制代码
其中ObjectPool采用了一种叫FreeList的数据结构来保存userdata和obj,除了不方便遍历外,其他操作如:增、删、查询都是                              O                      (                      1                      )                          O(1)               O(1)的复杂度,这里可以简单认为其就是一个Dictionary<int, object>。
下面代码显示了obj唯一id的产生过程。当一个obj第一次传递到lua之前,会调用ObjectTranslator.addObject函数来设置这objects和reverseMap这2个容器,并产生一个唯一id:
  1. // 文件:ObjectTranslator.csintaddObject(object obj,bool is_valuetype,bool is_enum){// 这个index可以理解为就是obj的唯一id(其实就是obj在ObjectPool的数组下标)// 后续可以通过objects.Get(index)查询到这个objint index = objects.Add(obj);if(is_enum){
  2.         enumMap[obj]= index;}elseif(!is_valuetype){// 反向引用,后续会讲到
  3.         reverseMap[obj]= index;}return index;}
复制代码
当obj真正被传到lua时,会调用到下面的ObjectTranslator.Push函数:
  1. // 文件:ObjectTranslator.cspublicvoidPush(RealStatePtr L,object o){if(o ==null){
  2.         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的唯一idif(needcache &&(is_enum ? enumMap.TryGetValue(o,out index): reverseMap.TryGetValue(o,out index))){// 这时可以直接将cache里的userdata传给luaif(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
  3.     index =addObject(o, is_valuetype, is_enum);// 为obj产生userdata,放入cache,再传给lua// 可以看到,第二个参数传的就是index,对应c代码里的key参数
  4.     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文件,有如下类似代码:
  1. // wrap文件,此时ud在栈的位置是1object __cl_gen_to_be_invoked = translator.FastGetCSObj(L,1);// 文件:ObjectTranslator.csinternalobjectFastGetCSObj(RealStatePtr L,int index){// xlua_tocsobj_fast函数会返回index位置的userdata里保存的值,也就是之前我们存入的obj唯一idreturngetCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index));}// 参数udata就是obj的唯一id了privateobjectgetCsObj(RealStatePtr L,int index,int udata){object obj;if(udata ==-1){if(LuaAPI.lua_type(L, index)!= LuaTypes.LUA_TUSERDATA)returnnull;Type type =GetTypeOf(L, index);if(type ==typeof(decimal)){decimal v;Get(L, index,out v);return v;}GetCSObjectget;if(type !=null&& custom_get_funcs.TryGetValue(type,outget)){returnget(L, index);}else{returnnull;}}elseif(objects.TryGetValue(udata,out obj))// 这里通过udata查找obj并返回{#if !UNITY_5 && !XLUA_GENERALif(obj !=null&& obj is UnityEngine.Object &&((obj as UnityEngine.Object)==null)){thrownewUnityEngine.MissingReferenceException("The object of type '"+ obj.GetType().Name +"' has been destroyed but you are still trying to access it.");}#endifreturn obj;}returnnull;}
复制代码
可以看下xlua_tocsobj_fast的源码,实现也非常简单:
  1. // 文件:xlua.c
  2. 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的构造函数:
  1. publicObjectTranslator(LuaEnv luaenv,RealStatePtr L){// some code ...// 下面代码可以理解为:// cache = setmetatable({}, { __mode = 'v' })
  2.     LuaAPI.lua_newtable(L);
  3.     LuaAPI.lua_newtable(L);
  4.     LuaAPI.xlua_pushasciistring(L,"__mode");
  5.     LuaAPI.xlua_pushasciistring(L,"v");
  6.     LuaAPI.lua_rawset(L,-3);
  7.     LuaAPI.lua_setmetatable(L,-2);
  8.     cacheRef = LuaAPI.luaL_ref(L, LuaIndexes.LUA_REGISTRYINDEX);initCSharpCallLua();}
复制代码
然后看下obj的引用。在交互过程中,会对obj产生引用的地方是ObjectTranslator.addObject函数里,objects和reverseMap都对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,看下面的代码:
  1. // 文件:StaticLuaCallbacks.cs[MonoPInvokeCallback(typeof(LuaCSFunction))]publicstaticintLuaGC(RealStatePtr L){try{// udata就是obj的唯一idint udata = LuaAPI.xlua_tocsobj_safe(L,1);if(udata !=-1){ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
  2.             translator.collectObject(udata);}return0;}catch(Exception e){return LuaAPI.luaL_error(L,"c# exception in LuaGC:"+ e);}}// 文件:ObjectTranslator.csinternalvoidcollectObject(int obj_index_to_collect){object o;if(objects.TryGetValue(obj_index_to_collect,out o)){// 清理objects
  3.         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){
  4.                     enumMap.Remove(o);}else{// 清理reverseMap
  5.                     reverseMap.Remove(o);}}}}}
复制代码
注意cache表不需要清理,因为它是弱表!
最后来看obj的GC处理。上面讲到objects和reveresMap都会对obj产生引用,也就是说这个obj就算c#层其他地方都不引用它了,它也不会被GC,直到lua层也没人引用obj对应的userdata了,userdata被GC导致objects和reverseMap被清理,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做一些清理。看下面的代码:
  1. // LuaEnv.cs#if !XLUA_GENERALint last_check_point =0;int max_check_per_tick =20;staticboolObjectValidCheck(object obj){// 如果obj是UnityEngine.Object而且==null,则任何其已经被Destroy了return(!(obj is UnityEngine.Object))||((obj as UnityEngine.Object)!=null);}
  2. Func<object,bool> object_valid_checker =newFunc<object,bool>(ObjectValidCheck);#endifpublicvoidTick(){#if THREAD_SAFT || HOTFIX_ENABLElock(luaEnvLock){#endifvar _L = L;lock(refQueue){while(refQueue.Count >0){GCAction gca = refQueue.Dequeue();
  3.                 translator.ReleaseLuaBase(_L, gca.Reference, gca.IsDelegate);}}#if !XLUA_GENERAL
  4.         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}publicvoidGC(){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是否失效了。
  1. -- unity对象判断为空, 如果你有些对象是在c#删掉了,lua不知道
  2. -- 判断这种对象为空时可以用下面这个函数。
  3. functionIsNil(uobj)return uobj == nil or uobj:Equals(nil)
  4. end
复制代码
最后的最后,分析一个容易出现的循环引用问题:
上面提到,如果obj需要保证userdata的生命周期,会存储一个userdata的引用,那这个引用应该什么时候释放呢?xLua里的LuaBase类也负责了lua引用的管理,它是采用Dispose和析构函数的方式来释放lua引用的。那我们也可以为obj实现Dispose和析构函数来释放引用吗?答案是不行的,这样会造成循环引用。由于obj自身被objects和reverseMap引用着,这个引用必须等到userdata被GC才会释放,而userdata却又被obj引用了,从而产生循环引用,所以obj根本不可能触发到析构函数,也无法释放userdata引用,最后造成内存泄漏。
怎么解决呢?可以通过提供一个统一的Destroy函数来做释放。所有Object不用时都需要调用这个函数来做清理工作,调用这个函数后,就可以认为其已经不能再使用了,这样就可以在这个Destroy函数里清理userdata的引用。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 09:08 , Processed in 0.088305 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表