• Unity 使用xLua遇到的坑


    在我们使用xLua作为Unity中lua集成的解决方案时,遇到了一个问题,就是当我们使用在lua中把UI中的某个控件绑定相应的事件(如按钮的onClick事件),xLua绑定这个事件是用委托实现的,具体代码可以查看xLua的代码。而在程序退出的时候xLua会检查对应的委托有没有被正确的释放掉,如果没有释放掉的话就会抛出异常。代码如表所示:

     
     1         public virtual void Dispose(bool dispose)
     2         {
     3 #if THREAD_SAFE || HOTFIX_ENABLE
     4             lock (luaEnvLock)
     5             {
     6 #endif
     7                 if (disposed) return;
     8                 Tick();
     9 
    10                 if (!translator.AllDelegateBridgeReleased())
    11                 {
    12                     throw new InvalidOperationException("try to dispose a LuaEnv with C# callback!");
    13                 }
    14 
    15                 LuaAPI.lua_close(L);
    16 
    17                 ObjectTranslatorPool.Instance.Remove(L);
    18                 translator = null;
    19 
    20                 rawL = IntPtr.Zero;
    21 
    22                 disposed = true;
    23 #if THREAD_SAFE || HOTFIX_ENABLE
    24             }
    25 #endif
    26         }

    这说明我们并没有把对应的委托给释放掉。所以我们需要确保在程序退出之前所有的委托要正确地释放掉。方案大体如下,每一个UI都对应一个实例,这样在绑定控件的时候创建一个匿名函数,这个函数用于控件把这个控件绑定的事件清除掉,同时把这个匿名函数放到一个数组里面去,在这个UI销毁的时候调用一个函数(比如我们叫做Destroy),这个函数的作用就是负责一些清理工作,其中就包括遍历前面提到的匿名函数的数组并挨个调用。这样就把xLua生成的委托的引用去掉了。在程序退出并触发GC的时候就会把这个委托释放掉,这样xLua检查就没有问题了。

     
     1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
     2     aButton.onClick.AddListener(
     3         function ()
     4             aFunc(aUIInstance)
     5         end)
     6 
     7     // 将闭包添加到一个table中用于后面调用
     8     table.insert(aUIInstance.unregisterWidgetClousures, 
     9         function()
    10             aButton.onClick:RemoveAllListeners()
    11         end)
    12 
    13 end

    可能到这里你觉得问题已经解决了,可是如果到这的话就不会有这篇文章了。问题是这样调用了以后在程序退出的时候还是会抛出异常。按正常来说这样做了就可以了,经过一番实验发现只要这个控件没有被触碰过那么就可以正常退出,如果触碰了就会抛出异常。一开始怀疑是xLua的问题但经过看代码确定不是它的问题。这个时候想到了可能Unity对这个委托做了缓存,虽然我上面把它清除掉了,但是Unity内部可能是做了缓存的。最开始没有去关注这个问题,而是想了另外一个办法直接把控件对应的事件给黑窑了。示例代码如下所示:

     1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
     2     aButton.onClick.AddListener(
     3         function ()
     4             aFunc(aUIInstance)
     5         end)
     6 
     7     // 将闭包添加到一个table中用于后面调用
     8     table.insert(aUIInstance.unregisterWidgetClousures, 
     9         function()
    10             aButton.onClick = nil
    11         end)
    12 
    13 end

    这样就解决了问题。但是后面发现我们要重用UI的时候由于我们重用的规则所致(UI的C#对象没有回收但是会回收,但是lua对象会回收),上面的这个地方就出问题了。当我们下次再要重新使用这个UI的时候,因为上面被置空了,接下来使用就有问题了。我们也想过其它的方法来解决,但总感觉破坏了原有简单的结构。这样做不太好。这个时候就想看看Unity到底哪里出了问题了,不过幸运的是很快就发现了问题。我们使用ILSpy打开UnityEngine.dll查看了一下UnityEvent的代码,发现在它的基类里面做了一个简单的优化,就是这个优化导致了上面问题的发生。我们来看下代码片断: 

    1 public abstract class UnityEventBase : ISerializationCallbackReceiver
    2 {
    3     private InvokableCallList m_Calls;
    4 }

    Unity用这个来保存需要调用函数,我们再来看看它的具体实现片段:

     1 namespace UnityEngine.Events
     2 {
     3     internal class InvokableCallList
     4     {
     5         private readonly List<BaseInvokableCall> m_PersistentCalls = new List<BaseInvokableCall>();
     6 
     7         private readonly List<BaseInvokableCall> m_RuntimeCalls = new List<BaseInvokableCall>();
     8 
     9         private readonly List<BaseInvokableCall> m_ExecutingCalls = new List<BaseInvokableCall>();
    10 
    11         private bool m_NeedsUpdate = true;
    12 
    13         public void AddListener(BaseInvokableCall call)
    14         {
    15             this.m_RuntimeCalls.Add(call);
    16             this.m_NeedsUpdate = true;
    17         }
    18 
    19         public void RemoveListener(object targetObj, MethodInfo method)
    20         {
    21             List<BaseInvokableCall> list = new List<BaseInvokableCall>();
    22             for (int i = 0; i < this.m_RuntimeCalls.Count; i++)
    23             {
    24                 if (this.m_RuntimeCalls[i].Find(targetObj, method))
    25                 {
    26                     list.Add(this.m_RuntimeCalls[i]);
    27                 }
    28             }
    29             this.m_RuntimeCalls.RemoveAll(new Predicate<BaseInvokableCall>(list.Contains));
    30             this.m_NeedsUpdate = true;
    31         }
    32 
    33         public void Clear()
    34         {
    35             this.m_RuntimeCalls.Clear();
    36             this.m_NeedsUpdate = true;
    37         }
    38 
    39         public void Invoke(object[] parameters)
    40         {
    41             if (this.m_NeedsUpdate)
    42             {
    43                 this.m_ExecutingCalls.Clear();
    44                 this.m_ExecutingCalls.AddRange(this.m_PersistentCalls);
    45                 this.m_ExecutingCalls.AddRange(this.m_RuntimeCalls);
    46                 this.m_NeedsUpdate = false;
    47             }
    48             for (int i = 0; i < this.m_ExecutingCalls.Count; i++)
    49             {
    50                 this.m_ExecutingCalls[i].Invoke(parameters);
    51             }
    52         }
    53     }
    54 }
    我们看到有m_RuntimeCalls这个变量,它是用来做什么的呢?就是为了做一个优化的,为了只在添加或移除了Listener之后才更新它做的一个优化。对于原来Unity本身的设计来说,是没有问题的。但是,我们看一下不论是RemoveListener或者Clear的时候都没有清掉m_RuntimeCalls里面的值,按理说它在Clear()的时候是应该清掉的。所以就有了我们前面提到的问题。知道了原因,这里就有了两个解决方法:
    1. 直接必UnityEngine.dll的代码,因为我们没有源码,所以只能通过一些工作来改。但这带来一个问题,就是需要给开发组的每个人替换修改后的dll,另外一个问题就是如果升级Unity的话又会带来不必要的麻烦。所以这个方案就放弃了。
    2. 我们可以看到虽然Clear()没有调用m_ExecutingCalls.Clear(),但是我们可以再调用一次Invoke()函数,这个时候它就会把m_ExecutingCalls的内容清掉了,这个时候就没有对象引用着xLua生成的委托了。这个方案目前来说还是比较好的。因为毕竟多调用一次的开销是可以接受的。

      于是代码变成了如下代码示例的样子:

     1 function UIUtils:AddButtonOnClick(aUIInstance, aButton, aFunc)
     2     aButton.onClick.AddListener(
     3         function ()
     4             aFunc(aUIInstance)
     5         end)
     6 
     7     // 将闭包添加到一个table中用于后面调用
     8     table.insert(aUIInstance.unregisterWidgetClousures, 
     9         function()
    10             aButton.onClick:RemoveAllListeners()
    11             aButton.onClick()
    12         end)
    13 
    14 end

    好的,到这里问题已经完美解决了。当然我们也可以简单的把抛异常的地方注释掉,但这肯定不是解决问题的正确方法。当然如果你也遇到这个问题并且有更好的方案也可以一起讨论。

  • 相关阅读:
    斐波纳契数列
    实现刮刮乐的效果
    简易版美图秀秀
    js 宏任务和微任务
    作业3 阅读
    作业2 结对子作业
    做汉堡
    练习一
    Java设计模式十八:代理模式(Proxy)
    Java设计模式二十:适配器模式(Adapter)
  • 原文地址:https://www.cnblogs.com/ghl_carmack/p/7350530.html
Copyright © 2020-2023  润新知