xlua Delegate 泄漏检查
xlua Delegate 泄漏检查
排查和定位xLua中Delegate没有销毁的情况
前言
笔者在开发项目时,发现在xLua Dispose时总是会有
DelegateBridge
没有移除的情况,这有很多原因导致的,其中除了有之前讨论过的《UnityEvent引起的内存泄漏》,也不乏一些日常写业务逻辑疏忽导致的。但无论哪种情况,在销毁xLua虚拟机前调用多次GC释放,而仍然存在的DelegateBridge
就代表原来Lua逻辑有未正常销毁的情况,这体现其背后可能存在内存泄漏等问题,而本篇文章就是研究如何去找到没有释放所引用的Lua代码位置。
InvalidOperationException: try to dispose a LuaEnv with C# callback!
源码方案
xLua在虚拟机销毁前,会先调用多次GC,然后再释放
ObjectTranslator
的对象。
//调用多次GC是为了让Lua和C#之间没有引用关系的对象得以释放。
public void Dispose()
{
FullGc();
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
Dispose(true);
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
}
而在
Dispose ObjectTranslator
时,会释放掉所有的DelegateBridge
。在释放每一个DelegateBridge
都会先确认是否存活(IsAlive),如果有一个则会抛出异常。
if (!translator.AllDelegateBridgeReleased())
{
throw new InvalidOperationException("try to dispose a LuaEnv with C# callback!");
}
由于信息有限,所以并不知道是哪个对象哪句代码所注册Delegate,因此需要扩展对应的检查。
输出Delegate信息
首先较为简单的方案就是把没有释放的Delegate信息输出出来,其中包括Method的名称、类型等。
//Class DelegateBridgeBase
public string GetMessage()
{
StringBuilder builder = new StringBuilder();
if(bindTo != null)
{
foreach (var item in bindTo)
{
if (item.Value != null)
{
builder.AppendFormat("key:{0} methodName:{1} FullyQualifiedName:{2}\n", item.Key, item.Value.Method.Name, item.Value.Method.DeclaringType.Name);
}
}
}
else if(firstValue != null)
{
builder.AppendFormat("key:{0} methodName:{1} FullyQualifiedName:{2}\n", firstKey, firstValue.Method.Name, firstValue.Method.DeclaringType.Name);
}
return builder.ToString();
}
//Class ObjectTranslator
public bool AllDelegateBridgeReleased(IntPtr L, ref string message)
{
StringBuilder builder = new StringBuilder();
bool result = true;
foreach (var kv in delegate_bridges)
{
if (kv.Value.IsAlive)
{
builder.AppendLine(delegateBridgeBase.GetInfo());
}
}
message = builder.ToString();
return result;
}
这样就可以获取到所有没有释放的
Delegate
信息了。
debug.traceback
虽然获取
Delegate
信息能找到一些蛛丝马迹,但其实还远远不够,因为相同类型的Delegate
实在太多了,而且如果是反射Warp的方式,类似methodName:__Gen_Delegate_Imp7 FullyQualifiedName:XLuaGenDelegateImpl0
的信息一点用都没有。这时候就需要增加其他方法来获取更精确的信息了,例如获取Lua堆栈信息。
lua有一个
debug.traceback()
的API是可以获取到当前Lua逻辑中的堆栈信息的。
//在C#端增加以下逻辑
private string GetStack(RealStatePtr L)
{
var oldTop = LuaAPI.lua_gettop(L);
int debug = LuaAPI.xlua_getglobal(L, "debug");
LuaAPI.xlua_pushasciistring(L, "traceback");
LuaAPI.xlua_pgettable(L, -2);
var index = LuaAPI.lua_pcall(L, 0, 1, 0);
string luaStack = LuaAPI.lua_tostring(L, -1);
LuaAPI.lua_pop(L, 2);
LuaAPI.lua_settop(L, oldTop);
return luaStack;
}
尝试在ObjectTranslator.CreateDelegateBridge()调用,输出日志:
stack traceback:
[C]:in local 'loadFun'
Script/B:12: in field'Fun2'
Script/A:32: in field'Fun1'
......
这样就可以在创建
DelegateBridge
时获取到调用的Lua代码堆栈,然后保存起来,等销毁时把没有释放的堆栈信息输出。
//将原来的CreateDelegateBridge方法改成私有,把weakReference作为out结果返回。
private object CreateDelegateBridge(RealStatePtr L, Type delegateType, int idx, out WeakReference weakReference)
{
...
}
//增加新的CreateDelegateBridge提供给外部调用
public object CreateDelegateBridge(RealStatePtr L, Type delegateType, int idx)
{
WeakReference weakReference = null;
var stack = this.GetStack(L);
var result = CreateDelegateBridge(L, delegateType, idx, out weakReference);
if(weakReference != null)
{
int hash = weakReference.GetHashCode(); //利用weakReference的哈希值来作为key
bridgesReferenceInfos[hash] = stack;
}
return result;
}
AllDelegateBridgeReleased
和ReleaseLuaBase
也需要根据weakReference.GetHashCode()处理(获取信息和释放),这里就不再赘述了。
debug.getinfo
其实使用
traceback
已经能完美解决问题了,为什么还有下文呢?这是因为笔者在接入到项目后,发现项目变的很卡,特别是在创建模块(场景、界面)时。在一轮抽丝剥茧后,发现了debug.traceback
是一个性能消耗巨大的API
,在Editor下十次调用就有100ms,这即使在Editor下也是不可接受的。
所以在一轮查找后,笔者找到了
debug.getinfo
的方法来实现功能。
-- 获取当前调用层级所在的lua文件路径
-- 1就是第一层
debug.getinfo(1, "S").source
-- 获取当前调用层级(代码堆栈)所在的行号
-- 1就是第一层
debug.getinfo(1, "l").currentline
在C#侧增加了接口,替代了原来的
GetStack()
。
private string GetInfo(RealStatePtr L, int maxLevel = 5)
{
luaStackBuffer.Clear();
var oldTop = LuaAPI.lua_gettop(L);
int debug = LuaAPI.xlua_getglobal(L, "debug");
LuaAPI.xlua_pushasciistring(L, "getinfo");
LuaAPI.xlua_pgettable(L, -2);
for (int i = 0; i < maxLevel; i++)
{
LuaAPI.lua_pushvalue(L, -1);
int level = i + 2; //1为C端,没必要输出
LuaAPI.xlua_pushinteger(L, level); //参数
LuaAPI.xlua_pushasciistring(L, "S"); //参数
LuaAPI.lua_pcall(L, 2, 1, 0);
if (LuaAPI.lua_isnil(L, -1)) //当前节点是nil,表明没有上一层
{
LuaAPI.lua_pop(L, 1);
break;
}
LuaAPI.xlua_pushasciistring(L, "source");
LuaAPI.xlua_pgettable(L, -2);
string luaPath = LuaAPI.lua_tostring(L, -1);
LuaAPI.lua_pop(L, 2);
LuaAPI.lua_pushvalue(L, -1);
LuaAPI.xlua_pushinteger(L, level); //参数
LuaAPI.xlua_pushasciistring(L, "l"); //参数
LuaAPI.lua_pcall(L, 2, 1, 0);
LuaAPI.xlua_pushasciistring(L, "currentline");
LuaAPI.xlua_pgettable(L, -2);
int line = LuaAPI.xlua_tointeger(L, -1);
luaStackBuffer.AppendFormat("{0}: {1}\n", luaPath, line);
LuaAPI.lua_pop(L, 2);
}
LuaAPI.lua_pop(L, 2);
LuaAPI.lua_settop(L, oldTop);
return luaStackBuffer.ToString();
}
@Script/B: 12
@Script/A: 32
测试了一下性能,比
traceback
好一点,但也有80ms/10次。这也是万万不可接受的。 所以调试了其他方案,最后可以使用linedefined
来替代currentline
。linedefined
与currentline
不一样的地方就是,只能获取到当前function开始所在的行号,而不能获得代码精确的行号。
-- 获取当前调用层级所在的lua文件路径
-- 1就是第一层
debug.getinfo(1, "S").source
-- 获取当前调用层级调用函数所在的行号
-- 1就是第一层
debug.getinfo(1, "S").linedefined
修改原来的
GetInfo()
。
private string GetInfo(RealStatePtr L, int maxLevel = 5)
{
luaStackBuffer.Clear();
var oldTop = LuaAPI.lua_gettop(L);
int debug = LuaAPI.xlua_getglobal(L, "debug");
LuaAPI.xlua_pushasciistring(L, "getinfo");
LuaAPI.xlua_pgettable(L, -2);
for (int i = 0; i < maxLevel; i++)
{
LuaAPI.lua_pushvalue(L, -1);
int level = i + 2; //1为C端,没必要输出
LuaAPI.xlua_pushinteger(L, level); //参数
LuaAPI.xlua_pushasciistring(L, "S"); //参数
var index = LuaAPI.lua_pcall(L, 2, 1, 0);
if(LuaAPI.lua_isnil(L, -1)) //当前节点是nil,表明没有上一层
{
LuaAPI.lua_pop(L, 1);
break;
}
LuaAPI.xlua_pushasciistring(L, "source");
LuaAPI.xlua_pgettable(L, -2);
string luaPath = LuaAPI.lua_tostring(L, -1);
LuaAPI.lua_pop(L, 1);
LuaAPI.xlua_pushasciistring(L, "linedefined");
LuaAPI.xlua_pgettable(L, -2);
int line = LuaAPI.xlua_tointeger(L, -1);
luaStackBuffer.AppendFormat("{0}: {1}\n", luaPath, line);
LuaAPI.lua_pop(L, 2);
}
LuaAPI.lua_pop(L, 2);
LuaAPI.lua_settop(L, oldTop);
return luaStackBuffer.ToString();
}
@Script/B: 5
@Script/A: 30
改进后的方案所得到的堆栈信息可能没有那么的准确(因为没有精确的堆栈行号),但有lua代码地址和函数名已经能大大缩小其范围,相对耗时(一百倍)来说,具有极高的性价比。 以下是三种方式的对比:
方案 | 耗时(ms/10次) |
---|---|
traceback | 112 |
getinfo(精确行号) | 83 |
getinfo(函数行号) | 1 |
总结
在通过一系列测试,最后采用了
debug.getinfo
的方式来获取堆栈,并且采用一个消耗极低的方式获取到对应的函数行号,从而方便定位到问题的模块。如果想要更精确可以和debug.traceback
结合处理,日常使用低耗版本监控,有需要定位时采用精确定位。
评论
还沒有留言。