在我们使用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

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

 

【转】Unity 使用xLua遇到的坑的更多相关文章

  1. Unity 使用xLua遇到的坑

    在我们使用xLua作为Unity中lua集成的解决方案时,遇到了一个问题,就是当我们使用在lua中把UI中的某个控件绑定相应的事件(如按钮的onClick事件),xLua绑定这个事件是用委托实现的,具 ...

  2. Unity下XLua方案的各值类型GC优化深度剖析

    转自:http://gad.qq.com/article/detail/25645 前言 Unity下的C#GC Alloc(下面简称gc)是个大问题,而嵌入一个动态类型的Lua后,它们之间的交互很容 ...

  3. 现学现卖】IntelliJ+EmmyLua 开发调试Unity中Xlua

    http://blog.csdn.net/u010019717/article/details/77510066?ref=myread http://blog.csdn.NET/u010019717 ...

  4. unity, Shader.Find的一个坑

    所以对于没有被任何东西引用,只靠在游戏运行时使用Shader.Find换上去的shader,为了双保险,可以首先放到resources文件夹里,另外,再在ProjectSettings->Gra ...

  5. unity, Collider2D.bounds的一个坑

    Note that this will be an empty bounding box if the collider is disabled or the game object is inact ...

  6. Unity 游戏框架搭建 (十) QFramework v0.0.2小结

    从框架搭建系列的第一篇文章开始到现在有四个多月时间了,这段时间对自己来说有很多的收获,好多小伙伴和前辈不管是在评论区还是私下里给出的建议非常有参考性,在此先谢过各位. 说到是一篇小节,先列出框架的概要 ...

  7. Unity Dotween build error

    unity这东西感觉挺坑 在mac上build的时候遇到error IL2CPP error for method 'System.Void DG.Tweening.DOTweenPath::DORe ...

  8. xLua热更新插件

    一.xLua插件下载安装 1.从GitHub上搜索并下载插件 2.将文件复制到unity中 3.检查是否有错误 二.在unity中调用lua 1.简单调用 在c#脚本中使用LuaEnv类可以运行lua ...

  9. Unity——Js和Unity互相调用

    Unity项目可以打包成WebGl,打包后的项目文件: Build中是打包后的Js代码: Index.html是web项目的入口,里面可以调整web的自适应,也可以拿去嵌套: TemplateData ...

随机推荐

  1. NSMutableArray和NSArray的常用方法及相互转换

    NSMutableArray和NSArray的常用方法及相互转换 // NSArray --> NSMutableArray NSMutableArray *myMutableArray = [ ...

  2. MySQL优化之Explain命令解读

    简述: explain为mysql提供语句的执行计划信息.可以应用在select.delete.insert.update和place语句上.explain的执行计划,只是作为语句执行过程的一个参考, ...

  3. iPhone 横竖屏切换,全屏播放的三种方式

    1. 调用系统自带的强制屏幕旋转不过还得在AppDelegate中重写下面方法 - (UIInterfaceOrientationMask)application:(UIApplication *)a ...

  4. php-预定义

    php预定义异常 Exception是所有异常的基类 属性 message:异常消息内容 code:异常代码 file:抛出异常的文件名 line:抛出异常在该文件的行号 ErrorException ...

  5. c/c++面试指导---c语法总结

    任何一门学科或者专业在学习的过程中都要把握总结框架,大家在面试c/c++职位过程中要应对各种企业的面试,回答企业面试官的各种技术问题.如何应对各种各样的关于c/c++的企业面试题目,从各种繁杂的题目中 ...

  6. LightOJ 1203--Guarding Bananas(二维凸包+内角计算)

    1203 - Guarding Bananas    PDF (English) Statistics Forum Time Limit: 3 second(s) Memory Limit: 32 M ...

  7. MySQL的空值和NULL区别

    从本质上区别: 1.空值不占空间 2.null值占空间   通俗的讲: 空值就像是一个真空转态杯子,什么都没有,而null值就是一个装满空气的杯子,虽然看起来都是一样的,但是有着本质的区别.     ...

  8. springboot-自定义起步依赖

    自定义起步依赖步骤: 1.  添加configuration注解文件 -          指定什么情况下加载配置 -          使用enableconfigurationProperties ...

  9. redhat系统升级openssh到7.5

    注意,注意,注意重要的事情说三遍,关于ssh的升级不能完全按照别人的教程进行升级,因为每台生产机器都是不一样的,有可能别人能升级成功但是另外一个就可能会失败,因为每台机器上面跑的应用是不一样的,涉及到 ...

  10. 笔记: js构造函数与原型

    目录 构造函数与原型介绍 涉及三种引用的操作 有关原型及原型链的一些相关方法总结 @ 构造函数与原型介绍 1.函数与函数的原型对象(prototype object): 在JavaScript中,创建 ...