ugui中常用的监听事件的组件大致有,button,ScrollRect,toggle,InputField,Slider
在使用lua写逻辑功能的时候避免不了要有点击相关的交互,而这些交互的函数在c#中其实是通过委托实现的,下面就以常用的button为例介绍一下:
local button = UnityEngine.UI.Button()
local btnHandler = function() print("test click") end
button.onClick:AddListener(btnHandler)
以上代码可能是大家比较熟悉的,那么AddListener究竟做了哪些工作呢,我们看一下:
public class UnityEngine_UI_Button_ButtonClickedEventWrap
{
public static void Register(LuaState L)
{
L.BeginClass(typeof(UnityEngine.UI.Button.ButtonClickedEvent), typeof(UnityEngine.Events.UnityEvent));
L.RegFunction("New", _CreateUnityEngine_UI_Button_ButtonClickedEvent);
L.RegFunction("__tostring", ToLua.op_ToString);
L.EndClass();
}
在看一下UnityEngine_Events_UnityEventWrap这个类
public class UnityEngine_Events_UnityEventWrap
{
public static void Register(LuaState L)
{
L.BeginClass(typeof(UnityEngine.Events.UnityEvent), typeof(UnityEngine.Events.UnityEventBase));
L.RegFunction("AddListener", AddListener);
L.RegFunction("RemoveListener", RemoveListener);
L.RegFunction("Invoke", Invoke);
L.RegFunction("New", _CreateUnityEngine_Events_UnityEvent);
L.RegFunction("__tostring", ToLua.op_ToString);
L.EndClass();
}
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int AddListener(IntPtr L)
{
try
{
ToLua.CheckArgsCount(L, 2);
UnityEngine.Events.UnityEvent obj = (UnityEngine.Events.UnityEvent)ToLua.CheckObject(L, 1);
UnityEngine.Events.UnityAction arg0 = (UnityEngine.Events.UnityAction)ToLua.CheckDelegate(L, 2);
obj.AddListener(arg0);
return 0;
}
catch (Exception e)
{
return LuaDLL.toluaL_exception(L, e);
}
}
继续往下看:
public static Delegate CheckDelegate(IntPtr L, int stackPos)
{
LuaTypes luatype = LuaDLL.lua_type(L, stackPos);
switch (luatype)
{
case LuaTypes.LUA_TNIL:
return null;
case LuaTypes.LUA_TFUNCTION:
LuaFunction func = ToLua.ToLuaFunction(L, stackPos);
return DelegateTraits.Create(func);
case LuaTypes.LUA_TUSERDATA:
return (Delegate)ToLua.CheckObject(L, stackPos, typeof(T));
default:
LuaDLL.luaL_typerror(L, stackPos, TypeTraits.GetTypeName());
return null;
}
}
class UnityEngine_Events_UnityAction_Event : LuaDelegate
{
public UnityEngine_Events_UnityAction_Event(LuaFunction func) : base(func) { }
public UnityEngine_Events_UnityAction_Event(LuaFunction func, LuaTable self) : base(func, self) { }
public void Call()
{
func.Call();
}
public void CallWithSelf()
{
func.BeginPCall();
func.Push(self);
func.PCall();
func.EndPCall();
}
}
接着就会走到这里:
public class LuaDelegate
{
public LuaFunction func = null;
public LuaTable self = null;
public MethodInfo method = null;
public LuaDelegate(LuaFunction func)
{
this.func = func;
}
public LuaDelegate(LuaFunction func, LuaTable self)
{
this.func = func;
this.self = self;
}
//如果count不是1,说明还有其他人引用,只能等待gc来处理
public virtual void Dispose()
{
method = null;
if (func != null)
{
func.Dispose(true);
func = null;
}
if (self != null)
{
self.Dispose(true);
self = null;
}
}
从上面看下来如果当button被删除之后如果onClick也就是UnityEngine.UI.Button.ButtonClickedEvent这个委托最后没有被删除的话,那么之前AddListener放进去的luafunc就永远不会删除
那么他会存在哪里呢,看如下代码:
public static LuaFunction ToLuaFunction(IntPtr L, int stackPos)
{
LuaTypes type = LuaDLL.lua_type(L, stackPos);
if (type == LuaTypes.LUA_TNIL)
{
return null;
}
stackPos = LuaDLL.abs_index(L, stackPos);
LuaDLL.lua_pushvalue(L, stackPos);
int reference = LuaDLL.toluaL_ref(L);
return LuaStatic.GetFunction(L, reference);
}
public static LuaFunction GetFunction(IntPtr L, int reference)
{
LuaState state = LuaState.Get(L);
return state.GetFunction(reference);
}
public LuaFunction GetFunction(int reference)
{
LuaFunction func = TryGetLuaRef(reference) as LuaFunction;
if (func == null)
{
func = new LuaFunction(reference, this);
funcRefMap.Add(reference, new WeakReference(func));
if (LogGC) Debugger.Log("Alloc LuaFunction name , id {0}", reference);
}
RemoveFromGCList(reference);
return func;
}
这样就很清晰的看到这个func被放到了funcRefMap中,而这个funcRefMap的话是在LuaState中,具体代码如下:
public class LuaState : LuaStatePtr, IDisposable
{
public ObjectTranslator translator = new ObjectTranslator();
public LuaReflection reflection = new LuaReflection();
public int ArrayMetatable { get; private set; }
public int DelegateMetatable { get; private set; }
public int TypeMetatable { get; private set; }
public int EnumMetatable { get; private set; }
public int IterMetatable { get; private set; }
public int EventMetatable { get; private set; }
//function ref
public int PackBounds { get; private set; }
public int UnpackBounds { get; private set; }
public int PackRay { get; private set; }
public int UnpackRay { get; private set; }
public int PackRaycastHit { get; private set; }
public int PackTouch { get; private set; }
public bool LogGC
{
get
{
return beLogGC;
}
set
{
beLogGC = value;
translator.LogGC = value;
}
}
public Action OnDestroy = delegate { };
Dictionary funcMap = new Dictionary();
Dictionary funcRefMap = new Dictionary();
Dictionary delegateMap = new Dictionary();
然而他们的释放却是在LuaState的Collect方法中:
protected void Collect(int reference, string name, bool beThread)
{
if (beThread)
{
WeakReference weak = null;
if (name != null)
{
funcMap.TryGetValue(name, out weak);
if (weak != null && !weak.IsAlive)
{
funcMap.Remove(name);
weak = null;
}
}
funcRefMap.TryGetValue(reference, out weak);
if (weak != null && !weak.IsAlive)
{
ToLuaUnRef(reference);
funcRefMap.Remove(reference);
delegateMap.Remove(reference);
if (LogGC)
{
string str = name == null ? "null" : name;
Debugger.Log("collect lua reference name {0}, id {1} in thread", str, reference);
}
}
}
else
{
if (name != null)
{
WeakReference weak = null;
funcMap.TryGetValue(name, out weak);
if (weak != null && weak.IsAlive)
{
LuaBaseRef lbr = (LuaBaseRef)weak.self;
if (reference == lbr.GetReference())
{
funcMap.Remove(name);
}
}
}
ToLuaUnRef(reference);
funcRefMap.Remove(reference);
delegateMap.Remove(reference);
if (LogGC)
{
string str = name == null ? "null" : name;
Debugger.Log("collect lua reference name {0}, id {1} in main", str, reference);
}
}
}
当然了,如果不满足gc条件那么这些func都会一直在内存中的,所以当使用完委托的时候一定要注意释放,不然如果luafun中包含其他的lua对象(lua闭包),那么都不会被释放掉的,当界面关闭的时候就要清理委托事件,如下:
button.onClick:RemoveAllListeners(),从逻辑上讲这就可以了,如果按钮不被点击,那么这是ok的,但是的我们在项目中发现了ugui有个bug,如果按钮被点击过,也就是说Invoke被触发了,那么这个委托怎么删除都删除不了,后来通过反编译工具看到UnityEventBase源码才发现,在执行invoke的时候委托会被缓存,只有下次再执行invoke的时候才被删除,具体源码如下:
namespace UnityEngine.Events
{
internal class InvokableCallList
{
private readonly List m_PersistentCalls = new List();
private readonly List m_RuntimeCalls = new List();
private readonly List m_ExecutingCalls = new List();
private bool m_NeedsUpdate = true;
public int Count
{
get
{
return this.m_PersistentCalls.Count + this.m_RuntimeCalls.Count;
}
}
public void AddPersistentInvokableCall(BaseInvokableCall call)
{
this.m_PersistentCalls.Add(call);
this.m_NeedsUpdate = true;
}
public void AddListener(BaseInvokableCall call)
{
this.m_RuntimeCalls.Add(call);
this.m_NeedsUpdate = true;
}
public void RemoveListener(object selfObj, MethodInfo method)
{
List baseInvokableCallList = new List();
for (int index = 0; index < this.m_RuntimeCalls.Count; ++index)
{
if (this.m_RuntimeCalls[index].Find(selfObj, method))
baseInvokableCallList.Add(this.m_RuntimeCalls[index]);
}
this.m_RuntimeCalls.RemoveAll(new Predicate(baseInvokableCallList.Contains));
this.m_NeedsUpdate = true;
}
public void Clear()
{
this.m_RuntimeCalls.Clear();
this.m_NeedsUpdate = true;
}
public void ClearPersistent()
{
this.m_PersistentCalls.Clear();
this.m_NeedsUpdate = true;
}
public void Invoke(object[] parameters)
{
if (this.m_NeedsUpdate)
{
this.m_ExecutingCalls.Clear();
this.m_ExecutingCalls.AddRange((IEnumerable) this.m_PersistentCalls);
this.m_ExecutingCalls.AddRange((IEnumerable) this.m_RuntimeCalls);
this.m_NeedsUpdate = false;
}
for (int index = 0; index < this.m_ExecutingCalls.Count; ++index)
this.m_ExecutingCalls[index].Invoke(parameters);
}
}
}
从上面代码中可以看出m_ExecutingCalls从来没有被清楚过,除非再次调用invoke,于是我们就想出了一个方案就是,在执行完button.onClick:RemoveAllListeners()之后再调用一下button.onClick:Invoke(),这样可以完美解决,上面的bug导致的内存泄漏,后来笔者又发现了一个方法也是可以达到同样的效果:
监听的时候:
button.onClick:AddListener(UnityEngine.Events.UnityAction( btnHandler))
销毁的时候:
button.onClick:RemoveAllListeners()
剩下的其他组件也是有以上两种方法处理,我们项目是这样处理的,具体代码如下:
--清理按钮监听事件
if self.__btnList then
for k,v in pairs(self.__btnList) do
if not tolua.isnull(v) then
v.onClick:RemoveAllListeners()
v.onClick:Invoke()
end
end
end
--清理toggle相关事件
if self.__toggleList then
for k,v in pairs(self.__toggleList) do
if not tolua.isnull(v) then
v.onValueChanged:RemoveAllListeners()
v.onValueChanged:Invoke(false)
end
end
end
--清理scrollRect相关事件
if self.__scrollList then
for k,v in pairs(self.__scrollList) do
if not tolua.isnull(v) then
v.onValueChanged:RemoveAllListeners()
v.onValueChanged:Invoke(UnityEngine.Vector2.zero)
end
end
end
--清理InputField相关事件
if self.__inputFieldList then
for k,v in pairs(self.__inputFieldList) do
if not tolua.isnull(v) then
v.onEndEdit:RemoveAllListeners()
v.onValueChanged:RemoveAllListeners()
v.onEndEdit:Invoke("")
v.onValueChanged:Invoke("")
end
end
end
--清理slider相关事件
if self.__sliderList then
for k,v in pairs(self.__sliderList) do
if not tolua.isnull(v) then
v.onValueChanged:RemoveAllListeners()
v.onValueChanged:Invoke(0)
end
end
end
最后再说一种情况,就是自定义的比如:
public Action onPointerClickEvent;
那么这一类属于系统的delegate,需要这样处理,
local luaClick= onPointerClickEvent
添加事件:
luaClick = btnHandler
销毁的时候:
luaClick:Destroy()
目前为止已经写完了,所有以上都可以通过打印验证,具体代码如下:
foreach (var item in funcMap)
{
LuaBaseRef temp = item.Value.Target as LuaBaseRef;
if (temp != null)
Debugger.LogError("funcMap::::"+item.Key+"-----------"+ (temp.name != null ? temp.name : "null"));
}
foreach (var item in funcRefMap)
{
LuaBaseRef temp = item.Value.Target as LuaBaseRef;
if (temp != null)
Debugger.LogError("funcRefMap::::" + item.Key + "-----------" + (temp.name != null ? temp.name : "null"));
}
foreach (var item in delegateMap)
{
LuaBaseRef temp = item.Value.Target as LuaBaseRef;
if (temp != null)
{
Debugger.LogError("delegateMap::::" + item.Key + "-----------" + (temp.name != null ? temp.name : "null"));
}
}