• 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

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

  • 相关阅读:
    【PAT甲级】1043 Is It a Binary Search Tree (25 分)(判断是否为BST的先序遍历并输出后序遍历)
    Educational Codeforces Round 73 (Rated for Div. 2)F(线段树,扫描线)
    【PAT甲级】1042 Shuffling Machine (20 分)
    【PAT甲级】1041 Be Unique (20 分)(多重集)
    【PAT甲级】1040 Longest Symmetric String (25 分)(cin.getline(s,1007))
    【PAT甲级】1039 Course List for Student (25 分)(vector嵌套于map,段错误原因未知)
    Codeforces Round #588 (Div. 2)E(DFS,思维,__gcd,树)
    2017-3-9 SQL server 数据库
    2017-3-8 学生信息展示习题
    2017-3-5 C#基础 函数--递归
  • 原文地址:https://www.cnblogs.com/ghl_carmack/p/7350530.html
Copyright © 2020-2023  润新知