【问题标题】:High memory allocations when unregistering delegates from event in C#从 C# 中的事件注销委托时的高内存分配
【发布时间】:2015-04-12 08:41:49
【问题描述】:

我们正在使用Unity3D 引擎开发一款游戏(它使用 Mono 作为用户代码 - 我们的代码是用 C# 编写的)。

场景是我们有一个暴露事件的类,大约有 250 个注册到该事件(游戏地图上的每个关卡对象都将自己注册到该事件):

// Every level registers itself (around 250 levels)
ScoresDataHelper.OnScoresUpdate += HandleOnScoresUpdate;

当销毁这个场景时,每个对象都会从事件中注销自己:

ScoresDataHelper.OnScoresUpdate -= HandleOnScoresUpdate;

当使用内置分析器时,我看到一个巨大的内存分配,深入挖掘表明这是由于未注册的代表。

我怀疑这是因为委托是不可变的,当将它们链接在一起时,会创建新实例?

这是 Unity 分析器的屏幕截图:

在处理大量事件订阅时,有什么办法可以避免这些内存分配?

【问题讨论】:

  • 有问题吗? GC 将在需要时立即启动并收集垃圾。顺便说一句,您想注销订阅该事件的每个对象还是只有少数对象?
  • 是的,这是一个问题,因为它分配了大约 3MB 并且还消耗时间。这使得从地图场景移动到实际游戏的体验需要更长的时间才能开始。
  • 嗯,请回答我的第二个问题“您想注销订阅该事件的每个对象还是只有少数对象?” Benjamin Gruenbaum 也为您提供了一个很好的答案。
  • 是的,都应该退订。当然,总是可以选择重新设计系统,以避免一开始就进行 250 次注册。我只是想从理论上了解如何处理许多正在注册的事件的问题,因为它似乎会产生如此高的内存分配。

标签: c# unity3d mono


【解决方案1】:

正如您在comments中确认要退订所有事件订阅,没有理由一一退订。

您可以将事件设置为空。这将取消订阅事件的所有内容,而不分配任何内存。

this.OnScoresUpdate = null;

需要注意的一点是,您不能从 ScoresDataHelper 类之外执行此操作。它必须在ScoresDataHelper 类中。

【讨论】:

  • 这适用于没有其他对象订阅此事件的情况。除了我提到的那 250 个对象之外,可能还有其他实例应该保持注册。否则 - 你是对的,这是最简单的方法。
  • @lysergic-acid 是的,你是对的。这就是为什么我在写这个作为答案之前在 cmets 中问了你两次。这仅在您需要取消订阅所有内容时才有效。
【解决方案2】:

一个简单的解决方案是一开始就不使用事件。

// T,S,U is whatever your function takes and returns
private List<Func<T,S,U>> Listeners = new List<Func<T,S,U>>();
public void OnScoresUpdate(Func<T,S,U> listener){
    Listeners.Add(listener);
}

// when you want to fire the event
foreach(var listener in Listeners){
    listener(param1, param2);
}

// when you want to unsubscribe the listeners:
Listeners = new List<Func<T,S,U>>();

如果您想避免内存问题,也可以使用弱收集,在元素被垃圾收集后立即自动删除侦听器。

【讨论】:

  • 这真是一个很好的答案。但值得注意的是,这还不是线程安全的,也没有解决如何取消订阅特定事件的问题。我不确定Listeners.Remove 是否有效。
  • 线程安全不是 Unity 关心的问题,因此对于这个特定场景,它可能是一个有效的场景。谢谢
  • 使其线程安全只是使用 ConcurrentQueue 的问题,所以这不是什么大不了的事,但很好:)
  • 让它成为线程安全的的。仅枚举 ConcurrentQueue 意味着在垃圾方面的巨大成本(使用常规 ListHashSet 是无法获得的)。
【解决方案3】:

如果您想取消订阅特定事件,您可以将集合更改为 HashSet。然后,您将删除一个侦听器,例如:

private HashSet<Func<T,S,U>> listeners = new HashSet<Func<T,S,U>>();

public void RemoveListener(Func<T,S,U> listener){listeners.remove(listener);}

【讨论】:

    【解决方案4】:

    添加此答案是因为这是目前 Google 上的最高结果:

    根据these tests,将函数添加到委托会产生 208 字节的垃圾,而将委托添加到 event 会产生 104 字节乘以添加的事件数。因此,添加到事件的第 10 个委托会产生 1kb 的垃圾!我想类似的退订测试会产生相同的结果。上面使用ListHashSet 实现自定义事件类的两个答案对于我自己在Unity 中的这个问题的版本来说是一个简单而有效的解决方案。

    【讨论】:

      猜你喜欢
      • 2016-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多