【问题标题】:precautions to take to prevent memory leaks due to added event handles为防止由于添加事件句柄而导致内存泄漏而采取的预防措施
【发布时间】:2012-04-22 01:59:24
【问题描述】:

我将制作一个 GUI,该 GUI 将动态创建一组控件并为其分配事件。我需要在运行时添加和删除这些控件。它看起来像这样:

FlowLayoutPanel.Controls.Clear();
<< add new controls, assigning Click events with += >>

我听说使用 += 分配事件处理程序会导致内存泄漏(更具体地说,在应用程序退出之前不会释放内存)。我想避免这种情况。我知道我可以编写一些函数,如How to remove all event handlers from a control 来查找所有事件处理程序并删除它们,但它看起来非常复杂。

还有其他方法吗?调用 Dispose 是否有助于删除这些事件处理程序?你能像在 C/C++ 中那样销毁对象以强制释放它们的内存吗?

谢谢!

PS:问题是,我不知道要分离什么事件。我将创建许多标签并向它们添加不同类型的 onclick 事件。当需要清理流布局面板时,无法知道哪个事件处理程序附加到哪个标签。

这是示例代码(_flowLP 是一个 FlowLayoutPanel) - 此 Refresh() 函数在应用程序退出之前运行了多次。

    private void Refresh()
    {
        Label l;
        Random rnd = new Random();

        // What code should i add here to prevent memory leaks
        _flowLP.Controls.Clear();

        l = new Label();
        l.Text = "1";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);

        l = new Label();
        l.Text = "2";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);

        l = new Label();
        l.Text = "3";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);

        l = new Label();
        l.Text = "4";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);

        l = new Label();
        l.Text = "5";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);

        l = new Label();
        l.Text = "6";
        if (rnd.Next(3) == 0) l.Click += Method1;
        if (rnd.Next(3) == 0) l.Click += Method2;
        if (rnd.Next(3) == 0) l.Click += Method3;
        _flowLP.Controls.Add(l);
    }

【问题讨论】:

  • 关于您的编辑:如果无法确定添加到事件调用列表的方法,您可以将已添加到事件处理程序的委托保留在某个集合中,并使用它来删除到时候代表。或者,如果您知道要完全清除事件的调用列表,只需完全清除它们即可。

标签: c# events memory memory-leaks


【解决方案1】:

当您将较短生命周期的事件消费者附加到较长生命周期的事件生产者时,这主要是一个令人担忧的问题。如果他们有相似的生活或与我描述的相反,这不是问题。

如果您确实担心这一点,只需使用 -= 从事件中分离。这将删除附件创建的引用,并有助于避免此类内存问题。

编辑:由于 cmets 有点长,我会在这里发布一些跟进。当你附加到一个事件时,你所做的就是在事件提供者上挂一个对你自己的引用。因此,例如,如果您有一个带有 StrikesMidnight 事件的 Clock 类,并且您从一个名为 Bedtime 的类中订阅该事件,则 Bedtime 的实际机制是 clock.StrikesMidnight += this.HandleMidnight; 是您正在为自己分配时钟引用。就好像 Clock 有一个对象属性,你说clock.ObjectProperty = this;

因此,在 Bedtime 类的生命周期很短并且超出范围的情况下,Bedtime 会显示出来,在 Clock 上挂起对自身的引用,然后超出范围。问题是,时钟仍然有它的引用,所以即使它超出了范围,垃圾收集器也不会收集就寝时间。

....

这就是背景。在您的情况下,您正在创建一个标签并将对您自己的引用附加到它(通过您的“MethodX”处理程序)。调用刷新时,您会清除标签列表(意味着它们超出范围)。它们超出了范围,并且通过 MethodX 处理程序引用了您的类,但那又如何呢?他们有引用并不能阻止他们被 GC'ed。没有人在您的代码中持有对它们的引用,因此 GC 将对它们进行处理并且您不会泄漏内存。

【讨论】:

  • 但我不知道要分离什么事件!那我该怎么办?
  • 这与您对 += 语句所做的操作正好相反。每次您将本地事件处理程序附加到某个协作者的事件时,请记住在您不再需要知道正在引发的事件时使用 -= 进行分离。如果需要,您可以在 dispose() 方法中执行此操作,但这不是绝对必要的。
  • 是的,但我不知道要分离哪种方法。比如说,我有 10 种方法,其中一种方法是根据当时的条件在不久前附在 20 个标签中的第 5 个标签上的。现在我需要从 flowlayoutpanel 中删除所有标签。我怎么知道我必须使用 10 个事件处理程序中的哪一个 -=?
  • 你能发布一些示例代码来说明你在说什么吗?这可能会让我更容易回复具体细节。
  • 所有代码都会占用大量空间,而且不相关。试想一下:创建了 20 个标签,每个标签从 10 个事件处理程序列表中随机获取一个 Click 事件处理程序,称为 Foo1..Foo10,每个标签都被添加到 flowlayoutpanel。然后,在 2 分钟内,我需要清除 flowlayout 面板并再次执行此操作。我如何确保前 20 个标签拥有的内存是属性 GCed?我不能使用 -= ,因为它们是随机附加的,而且我没有在内存中存储有关谁得到了什么的信息。
【解决方案2】:

只要您处理包含表单,垃圾收集器就应该清理所有控件。

在控件上订阅事件不会使控件保持活动状态,因为控件具有对处理委托的引用;委托没有对控件的引用。

事件订阅会阻止控件被清理的情况是,控件中的某些代码订阅了它所包含的表单实例之外的事件。例如,如果自定义组合框订阅到静态类上的事件以让控件知道何时应更新其选项列表。如果自定义控件没有取消连接此事件,它将在应用程序期间被静态类事件寄存器引用。由于控件将引用其容器(等等),因此整个表单可能会保留在内存中。在这种情况下,控件应在其 Dispose 方法中取消连接事件。订阅静态类或长寿命实例类上的事件应始终引发红旗,并在不再需要时明确取消连线。

在表单中,只要您的所有事件都连接到表单类或范围由表单实例控制的对象上的实例方法,那么当表单作为根的对象图时,控件将被清理超出范围。请注意,外部类在不再需要表单后不会保留对表单的引用。

【讨论】:

  • 换句话说,如果一个超出范围的对象有另一个对象的方法订阅了它的事件,它会被释放和GCed就好了,但是如果一个超出范围的对象有一些它的方法订阅了其他一些活动对象的事件,那么它不会被 GCed?
  • 这非常正确。如果他们订阅事件的对象符合收集条件,他们仍然会被垃圾收集。否则有资格被收集的对象之间的循环引用不会阻止它们被收集。
【解决方案3】:

我的第一个建议是不要预先优化。在尝试解决它之前,请确保这是一个问题。

关于 Dispose():Dispose 应该用于在对象被垃圾回收之前释放非托管资源。请参阅http://msdn.microsoft.com/en-us/library/system.idisposable.aspx 了解更多信息。

为防止事件处理程序保留对控件的引用,您需要让控件取消订阅它们在创建时订阅的所有事件处理程序。提示:如果您通过 GUI 添加事件处理程序,请调查自动生成的代码以查看在调用 FlowLayoutPanel.Controls.Clear() 之前需要取消订阅的内容。一种干净(-er?)的方法是创建您自己的控件,该控件继承自所需控件并添加一个“Cleanup()”方法,该方法取消订阅任何已订阅的事件处理程序(您应该知道哪个事件处理程序已被订阅 - 要么是因为您编写了代码,要么是为您生成的)。

【讨论】:

  • 不,正如我所说,我正在动态执行此操作,所以不是通过 GUI。
猜你喜欢
  • 2012-02-15
  • 2012-05-01
  • 2013-11-23
  • 2021-04-09
  • 1970-01-01
  • 2018-07-30
  • 2010-09-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多