今天来介绍一个lua里面的新机制:弱引用表(weak table)。
当时在找内存泄漏的解决方案的时候了解到了这个lua机制。的确它在某些内存泄漏问题中可以发挥强大的作用,但是在项目中不太能单纯用这个机制解决现有的问题(组合类没有绑定luabehaviour,且在其gameobject被destroy掉时因没有触发某绑定了luatable的按钮的生命周期ondestroy事件导致c#侧持有lua侧userdata而无法释放)。
首先明确一点:Lua语言自身有垃圾回收机制。当lua侧某个变量没有再被其他地方引用到的时候,它将会被自动回收。比如下面一段代码:
A = Class.class("A")
A.b = nil
B = Class.class("B")
a = A.new()
a.b = B.new()
a.b.c = 1
a.num = 1
collectgarbage()
print(a.num) --print:1
print(a.b.c) --print:1
a = nil
这里由a来创建一个B对象b,在我们中途去干一些操作的时候这个b并不会被消失,因为lua内部记录了这个对象引用数为1,不为0的对象它是不敢乱动的。
而后a被赋值为nil的时候我们不需要手动设置b为nil,因为已然没有其他地方持有这个对象了,即引用数为0,所以被当作垃圾清理掉了。
注意:引用概念只存在于tables、userdata、functions。
lua为我们提供了一种手段,在创建弱引用表的时候,内部的某些被引用到的对象其引用数并不增加。如上方的a如果是弱引用表并设置value为弱引用的话,那么在回收之后想要访问到这个表内引用的对象是无法行得通的(只有有引用概念的类型才受到影响)。
A = Class.class("A")
A.__mode = "kv"
A.b = nil
B = Class.class("B")
a = A.new()
a.b = B.new()
a.b.c = 1
a.num = 1
print(a.num) --print:1
print(a.b.c) --print:1
collectgarbage()
print(a.num) --print:1
print(a.b.c) --error:attempt to index a nil value (field 'b')
a = nil
同样的,function也会被回收:
A = Class.class("A")
A.__mode = "kv"
a = A.new()
function a:Test()
print("test")
end
a:Test() --print:test
collectgarbage()
a:Test() --error:method 'Test' is not callable (a nil value)
a = nil
为什么有了垃圾回收机制,还要有这个不增加引用数的weak table机制?
lua文档这样解释的:
垃圾收集器只能在确认对象失效之后才会进行收集;它是不会知道你对垃圾的定义的。一个典型的例子就是堆栈:有一个数组和指向栈顶的索引构成。你知道这个数组中有效的只是在顶端的那一部分,但Lua不那么认为。如果你通过简单的出栈操作提取一个数组元素,那么数组对象的其他部分对Lua来说仍然是有效的。同样的,任何在全局变量中声明的对象,都不是Lua认为的垃圾,即使你的程序中根本没有用到他们。这两种情况下,你应当自己处理它(你的程序),为这种对象赋nil值,防止他们锁住其他的空闲对象。
表的weak性由他的metatable的__mode域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个table中的keys就是weak的;如果这个字符串包含小写字母‘v’,这个table中的vaules就是weak的。
设置成k:
key = {name = "key"}
test = {}
test[key] = 1
for k, v in pairs(test) do
print(v) --print:1
end
key = nil
collectgarbage()
for k, v in pairs(test) do
print(v) --print:1
end
key = {name = "key"}
test = {}
setmetatable(test,{__mode = "k"})
test[key] = 1
for k, v in pairs(test) do
print(v) --print:1
end
key = nil
collectgarbage()
for k, v in pairs(test) do
print(v) -- no print
end
设置成v:
key = {name = "key"}
test = {}
test[1] = key
for k, v in pairs(test) do
print(v) --print:table: 0000020DC15B2B30
end
key = nil
collectgarbage()
for k, v in pairs(test) do
print(v) --print:table: 0000020DC15B2B30
end
key = {name = "key"}
test = {}
setmetatable(test,{__mode = "v"})
test[1] = key
for k, v in pairs(test) do
print(v) --print:table: 0000020DC15B2B30
end
key = nil
collectgarbage()
for k, v in pairs(test) do
print(v) --no print
end
同样对function适用:
key = {name = "key"}
test = {}
setmetatable(test,{__mode = "v"})
function test.func()
print("test.func")
end
test.func() --print:test.func
collectgarbage()
test.func() --error:field 'func' is not callable (a nil value)
如果是经常需要使用的一些global table,就可以使用记忆函数来优化存储空间:
local spriteAtlas = LuaAsset.LoadSpriteAtlas(“TestAB/Atlas/UI”)
imgTest.sprite = spriteAtlasProp:GetSprite(“test”)
之前已经说过,如果要走一遍wrap的装箱拆箱操作,这种是非常耗时的,所以我们可以利用弱引用来进行spriteAtlas的存储:
spriteAtlas = {}
setmetatable(spriteAtlas, {__mode = "v"})
function spriteAtlas.LoadSpriteAtlas(path)
if spriteAtlas[path] == nil then
spriteAtlas[path] = LuaAsset.LoadSpriteAtlas(path)
end
return spriteAtlas[path]
end
function spriteAtlas.GetSprite(atlasName,key)
if spriteAtlas[key] == nil then
spriteAtlas[key] = spriteAtlas.LoadSpriteAtlas(atlasName):GetSprite(key)
end
return spriteAtlas[key]
end
我们可以将同样的sprite结果存储在同一个table中,这样做可以省下很长时间的加载。还有一个有趣的结果就是,当我们不再使用那些不频繁的sprite(比如只会用到一次的sprite,之后再存储它就完全没有必要)的时候,这个table会直接清理掉而非等到这个进程完全结束,因为垃圾收集器一次次的在清理这个table。然而,只要某个key值对应的sprite正在被使用,它就不会从table中被移除。所以,这对于我们项目的空间&时间优化会显得更加智能。其他资源同理。
xlua里有一个概念叫:cache表,不会对obj造成任何引用。
具体留到之后下次有机会介绍xlua obj引用的时候讲。
当我们既需要对某些对象进行管理又不希望对其回收造成一定困扰,就可以使用弱引用。以下两个场景就是典型的例子,当然,还有上述“用法”中的例子。
之前踩坑内存泄漏的原因和button绑定event有很大关系,提到的其中一个解决方案就是建立一个weak table用来管理内部类的那些button,在其外部类被destroy的时候调用方法将这些button绑定的事件清空,可以避免内存泄漏问题。
代码:
------------------------------UIListener---------------------------
UIListener = {}
UIListener.eventGroup = {} --管理event的集合,在table.destroy中会按key释放
setmetatable(UIListener.eventGroup,{__mode = "k"})
--点击事件
function UIListener.AddButtonListener(btn,luaTable,luaFunction,luaTargetTable)
btn.onClick:AddListener(function()
if luaTable then
luaFunction(luaTable)
else
luaFunction()
end
end)
UIListener.AddEvent(btn.onClick,luaTargetTable)
end
--toggle事件
function UIListener.AddToggleListener(toggle,luaTable,luaFunction,luaTargetTable)
toggle.onValueChanged:AddListener(function(value)
if luaTable then
luaFunction(luaTable,value)
else
luaFunction(value)
end
end)
UIListener.AddEvent(toggle.onValueChanged,luaTargetTable)
end
--添加event事件到集合中
function UIListener.AddEvent(event,luaTargetTable)
if luaTargetTable and event then
if UIListener.eventGroup[luaTargetTable] == nil then
--@RefType luaIde#CS.UnityEngine.UI.Button<>
UIListener.eventGroup[luaTargetTable] = {}
end
table.insert(UIListener.eventGroup[luaTargetTable],event)
end
end
--释放某table下所有event listener
function UIListener.RemoveEventListeners(luaTable)
if UIListener.eventGroup and UIListener.eventGroup[luaTable] then
for k, v in pairs(UIListener.eventGroup[luaTable]) do
if v then
v:RemoveAllListeners()
end
end
UIListener.eventGroup[luaTable] = nil
end
end
return UIListener
------------------------------table---------------------------
function table.destroy(tbTarget)
if not tbTarget then
return
end
--释放这个table下所有event listener
UIListener.RemoveEventListeners(tbTarget)
local stype = nil
for key, value in pairs(tbTarget) do
if value then
stype = type(value)
if stype and (stype == "userdata" or stype == "table") then
tbTarget[key] = nil
end
end
end
end