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

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

时间: 2024-10-23 18:42:43

Unity 使用xLua遇到的坑的相关文章

unity, Shader.Find的一个坑

所以对于没有被任何东西引用,只靠在游戏运行时使用Shader.Find换上去的shader,为了双保险,可以首先放到resources文件夹里,另外,再在ProjectSettings->Graphics里的always included shaders添加此shader. 放在resources文件夹里能保证shader被打到apk里. 添加到always included shaders里不但能保证被打到apk里,还能使shader预加载,避免在第一次执行Shader.Find的时候出现卡顿

Unity使用Xlua框架热更

1.lua  IDE Intellij Idea编辑调试Lua https://blog.csdn.net/wwlcsdn000/article/details/80572683 原文地址:https://www.cnblogs.com/white-L/p/12162847.html

Unity常见面试题大全

1.       [C#语言基础]请简述拆箱和装箱. 答: 装箱操作: 值类型隐式转换为object类型或由此值类型实现的任何接口类型的过程. 1.在堆中开辟内存空间. 2.将值类型的数据复制到堆中. 3.返回堆中新分配对象的地址. 拆箱操作: object类型显示转换为值类型或从接口类型到实现该接口值类型的过程. 1.判断给定类型是否是装箱时的类型. 2.返回已装箱实例中属于原值类型字段的地址. 2.        [.NET(C#)] attribute,property,markup,ta

[2019.3] 也许是一个好的开始,也许不过如此

今天是本月的最后一天,也正好是周日,进行一下本月的总结吧. ? 工作上,这个月正式开始做新项目,新项目采用了Unity+C#+XLua的方式,这就意味着要开始学习Lua语言.从这个月的接触和使用来看,Lua是一门小巧方便的脚本语言,面向过程的设计,让我想起了写嵌入式C语言时的感觉.但是在需要使用面向对象的场合,就需要在编程思路上作一些转变了. 面向对象的三要素封装,继承和多态,看起来Lua只支持继承.封装的特性我没有去过多的探究,感觉上一个table里面也不可能设置某个元素为私有和公有等.多态特

AssetBundle异步加载被中断的问题

刘 刘泰言创建于 1 年前 在使用异步接口 yield return AssetBundle.ASyncLoad的时候,难免会想到:这个异步处理完之前如何Cancel掉这个任务?也就是一个AssetBundle加载到一半,现在要放弃加载,应该怎么处理? UnityAssetBundle 赞同 0评论 分享 10条回复 曾 曾毅回答于 1 年前 无法CANCEL 赞同 1评论 分享 Walker回答于 1 年前 Unity的接口里没有中断操作,但是可以在自己项目的ABMgr模块给业务逻辑层提供一个

Lua Profiler——快速定位Lua性能问题

导读 随着Lua在项目中的大量使用,它所带来的性能问题也逐步成为了项目运行时的重大性能瓶颈之一.特别是内存相关的性能问题,无论是内存分配过大还是内存泄露无法回收,目前都已经在不少研发项目中集中爆发. UWA推出的GOT Online中的Lua模式已经慢慢成为研发团队对Lua进行日常性能监控的有效手段.因此,也有越来越多的团队反馈,在监控到table数持续上涨,引用Mono对象持续增多等等问题时,应该如何快速地解决? 本次博物纳新推荐的开源库项目:LuaProfiler-For-Unity,相信可

Google Firebase Unity接入的坑

就说跑demo碰到的坑吧 https://firebase.google.com/docs/unity/setup 这是Firebase Unity的setup指南 大概写写步骤: 1. Firebase Console(https://console.firebase.google.com/)里设置好你的api key, 创建好app 2. 下载好GoogleService-Info.plist(IOS),google-services.json(Android),扔进Unity工程目录, 这

避开Unity的坑

1.制作抽象的prefab来做关卡编辑 尽可能制作抽象的prefab来做关卡编辑,该prefab应该足够抽象简单(只有一个GameObject,然后通过Gizmo来绘制是个不错的手段),否则以后变化的时候(常见的就是改美术资源),所有关卡都lost prefab,那么对策划来说是一场灾难.可以考虑通过数据表+编辑器的方式来提供策划操作同时也不再需要担心lost prefab的问题.prefab越简单抽象越不容易丢失,prefab之间嵌套的正确方式是通过链接而不是挂在节点下面. 2.尽可能避免修改

[Unity XLua]热更新XLua入门(二)-俄罗斯方块实例篇

前言 在xLua没出来之前,开源的lua框架基本都是以界面用Lua开发为主,核心战斗用C#开发,但xLua出来之后主推C#开发,Lua用作HotFix,这里我展示的第一个例子就是基于界面的经典2D小游戏--俄罗斯方块,界面逻辑是用C#写,启动加载逻辑是用lua,后面我会继续第二个同样的Demo,但是以纯Lua为主,这个案例明天更新. 效果图 由于我不会美术,所以这里使用的开源的游戏资源,感谢此作者. 游戏启动 C#启动Lua逻辑 using UnityEngine; using System.C