【问题标题】:Binding to list causes memory leak绑定到列表会导致内存泄漏
【发布时间】:2013-10-30 22:30:47
【问题描述】:

当我将 ListBox 的 ItemsSource 绑定到 List 时,绑定引擎会在控件消失后保留列表元素。这会导致所有列表元素都保留在内存中。使用 ObservalbleCollection 时问题就消失了。为什么会这样?

window标签内的xaml

<Grid>
    <StackPanel>
        <ContentControl Name="ContentControl">
            <ListBox ItemsSource="{Binding List, Mode=TwoWay}" DisplayMemberPath="Name"/>
        </ContentControl>
        <Button Click="Button_Click">GC</Button>
    </StackPanel>
</Grid>

后面的代码:

public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }

private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.DataContext = null;
        ContentControl.Content = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

视图模型

class ViewModel : INotifyPropertyChanged
{
    //Implementation of INotifyPropertyChanged ...

    //Introducing ObservableCollection as type resolves the problem
    private IEnumerable<Person> _list = 
            new List<Person> { new Person { Name = "one" }, new Person { Name = "two" } };

    public IEnumerable<Person> List
    {
        get { return _list; }
        set
        {
            _list = value;
            RaisePropertyChanged("List");
        }
    }

class Person
{
    public string Name { get; set; }
}

编辑:为了检查人员距离的泄漏,我使用了 ANTS 和 .Net 内存分析器。两者都表明在按下 GC 按钮后,只有绑定引擎持有对 person 对象的引用。

【问题讨论】:

  • “控制已消失”是什么意思?它变得隐形了吗?卸载了吗?
  • 你怎么知道这里的内存泄漏?你用什么工具来分析这个?
  • 在使用 List 时如何识别内存泄漏。
  • 您应该将列表从 IEnumerable 更改为 ObservableCollection

标签: c# wpf memory binding memory-leaks


【解决方案1】:

啊啊啊抓到你了。现在我明白你的意思了。

您将 Content 设置为 null,因此您终止了强制 ListBox,但 ItemsSource 仍绑定到 List,因此 ListBox 内存未完全释放。

不幸的是,这是一个众所周知的问题,并且在 MSDN 上也有详细记录。

如果您没有绑定到 DependencyProperty 或实现 INotifyPropertyChanged 或 ObservableCollection 的对象,则绑定可能会泄漏内存,完成后您必须取消绑定。

这是因为如果对象不是 DependencyProperty 或未实现 INotifyPropertyChanged 或未实现 INotifyCollectionChanged(普通列表未实现此),则它通过 PropertyDescriptors AddValueChanged 方法使用 ValueChanged 事件。这会导致 CLR 创建从 PropertyDescriptor 到对象的强引用,并且在大多数情况下,CLR 会在全局表中保留对 PropertyDescriptor 的引用。

因为绑定必须继续监听变化。此行为使 PropertyDescriptor 和对象之间的引用保持活动状态,因为目标仍在使用中。这可能会导致对象以及该对象所引用的任何对象中的内存泄漏。

问题是……Person 是否实现了 INotifyPropertyChanged?

【讨论】:

  • Person 没有实现 INotifyPropertyChanged。但我对这个众所周知的问题的理解不同。我知道提供属性的类应该实现 INotifyPropertyChanged,它确实如此。 “它通过 PropertyDescriptors AddValueChanged 方法使用 ValueChanged 事件”只有在提供属性的类是有意义的时候才有意义,对吧?
  • 不,不是真的,你快到了。像这样思考:ViewModel 中的每个属性都有一个类型。该类型可能是另一个实现 INotifyPropertyChanged 的​​类。 ViewModel 本身也应该实现 INotifyPropertyChanged。当 Binding 初始化时,它采用属性的类型并寻找 INotifyPropertyChanged 或 INotifyCollectionChanged 之类的接口。 List is at type 没有接口,而 ObservableCollection 有。如果没有找到接口,它将采用强引用(并非在每种情况下)。有时 Binding 采用弱引用。总结它的拥有属性的类型。
  • 非常感谢您花时间回答我的问题。我测试了您的原始理论,该理论与“实现 INotifyPropertyChanged 的​​人”有关。它没有。 Person 对象是否保存在内存中与 Person 是否实现 INotifyPropertyChanged 无关。它只取决于列表的类型。输入 List 表示它会留下,输入 ObservableCollection 表示它会消失。
  • 我对ValueChanged事件的静态引用问题的理解是这样的:ValueChanged是一个PropertyDescriptor的事件,它代表一个属性——绑定源对象的属性。此示例中的 Binding 源是 ViewModel。但是,它属于 INotifyPropertyChanged 类型,因此问题不适用于此处。
  • 再读一遍我写的。当然,即使您在 Person 中实现 INotifyPropertyChanged,它也不会改变任何东西。你一直误解我在说什么。 Binding 本身会查看您的情况 List<..> 中的属性类型,并询问该类型是否具有接口实现。它不会因此 Binding 在您的情况下创建一个强引用。如果属性的类型是一个确实实现了 INotifyPropertyChanged 的​​类,例如 Person 单独(不在列表中并且具有实现的接口),它将起作用。 List 是一个没有 INotifyCollectionChanged 接口的类。
【解决方案2】:

这是一个旧帖子,我明白了。但是,特别是接受的答案提供的解释不是很准确,其含义是错误的。

摘要

事先,这不是真正的内存泄漏。对于未实现 INotifyCollectionChanged 及其关联的 CollectionView 的集合,特殊绑定引擎的生命周期管理会妥善处理分配的内存。
WPF 支持绑定到许多不同的类型,例如 DataTable 和 XML,或者通常绑定到实现 IListIEnumerableIListSource 的类型。如果这是一个严重的错误,那么所有这些绑定都是危险的。
Microsoft 将在其文档中传播警告,例如绑定到 DataTable,就像在事件或数据绑定的上下文中潜在内存泄漏的情况下一样。

确实,当绑定到 INotifyCollectionChanged 类型的集合时,可以避免这种特殊行为 - 或者通过避免为未实现 INotifyCollectionChanged 的集合创建 CollectionView
观察到的行为实际上是由绑定引擎的实际 CollectionView 管理而不是数据绑定本身引起的。

以下代码触发与绑定到List\&lt;T&gt; 相同的行为:

var list = new List<int> {1, 2, 3};
ICollectionView listView = CollectionViewSource.GetDefaultView(list);
list = null;
listView = null;
for (int i = 0; i < 4; i++)
{
  GC.Collect(2, GCCollectionMode.Forced, true);
  GC.WaitForPendingFinalizers();
}

结果:整个集合引用图和CollectionView 仍在内存中(请参阅下面的说明)。
这应该证明该行为不是由数据绑定引入的,而是绑定引擎的CollectionView管理引入的。


数据绑定上下文中的内存泄漏

数据绑定的内存泄露问题与属性类型无关,与绑定源实现的通知系统有关。
来源必须
a) 参与依赖属性系统(通过扩展DependencyObject 并将属性实现为DependencyProperty)或
b) 实现INotifyPropertyChanged

否则,绑定引擎将创建对源的静态引用。静态引用是根引用。由于它们在应用程序生命周期内可访问的性质,因此此类根引用(如静态字段和它们引用的每个对象(内存))将永远无法进行垃圾收集,从而造成内存泄漏。

收藏和CollectionView 管理

收藏是另一回事。 所谓的泄漏的原因不是数据绑定本身。绑定引擎还负责创建实际集合的CollectionView
CollectionView 是在绑定上下文中创建还是在调用 CollectionViewSource.GetDefaultView 时创建:它是创建和管理视图的绑定引擎。

collection和CollectionView的关系是单向依赖,CollectionView知道collection是为了同步自己,而collection不知道CollectionView

每个现有的CollectionView 都由ViewManager 管理,它是绑定引擎的一部分。为了提高性能,视图管理器缓存视图:它使用WeakReference 将它们存储在ViewTable 中,以允许它们被垃圾回收。

当一个集合实现INotifyCollectionChanged

           │══════ strong reference R1.1 via event handler ═══════▶⎹
Collection │                                                        │ CollectionView
           │◀═══  strong reference R1.2 for lifetime management ═══⎹       ̲ ̲          
                                                                            △                                                                                                                  
                                                                            │
                                                                            │                                 
                                   ViewTable │───── weak reference W1 ──────┘

如果此集合实现INotifyCollectionChanged,则CollectionView 本身就是来自底层源集合的强引用R1.1 的目标。
这个强引用 R1.1 是由 CollectionView 在观察到 INotifyCollectionChanged.CollectionChanged 事件时创建的(通过附加集合存储的事件回调以便在引发事件时调用它)。

这样,CollectionView 的生命周期与集合的生命周期耦合:即使应用程序没有对 CollectionView 的引用,由于这种强引用,CollectionView 的生命周期会延长到收集本身有资格进行垃圾收集。
由于CollectionView 实例以WeakReference W1 的形式存储在ViewTable 中,因此这种生命周期耦合可防止WeakReference W1 过早地收集垃圾。
换句话说,这种强耦合R1.1 防止CollectionView 在收集之前 被垃圾收集。

此外,管理器还必须保证,只要应用程序引用了CollectionView,即使不再引用该集合,底层集合也会继续存在。这是通过保持从 CollectionView 到源集合的强引用 R1.2 来实现的。
无论集合类型如何,此引用始终存在。

当一个集合没有实现INotifyCollectionChanged

Collection │◀═══  strong reference R2.1 for lifetime management ════│ CollectionView
                                                                           ̲ ̲                                                                             
                                                                            ▲
                                                                            ║
                                                                            ║
                                 ViewTable │════ strong reference R2.2 ═════╝

现在,当集合没有实现INotifyCollectionChanged,则从集合到CollectionView 所需的强引用不存在(因为不涉及事件处理程序)并且WeakReference 存储在ViewTableCollectionView 中可能会过早地进行垃圾收集。
要解决此问题,视图管理器必须“人为地”保持CollectionView 活动。

它通过将强引用 R2.2 存储到CollectionView 来实现这一点。此时,视图管理器存储了对CollectionView 的强引用R2.2(由于缺少INotifyCollectionChanged),而这个CollectionView 具有强引用R2。 1 到基础集合。
这导致视图管理器保持 CollectionView 活动 (R2.2),因此 CollectionView 保持底层集合活动 (R2.1):这是感知到内存泄漏的原因。

但这并不是真正的泄漏,因为视图管理器通过注册强引用 R2.2 控制强引用 R2.2CollectionView 的生命周期em> 有到期日。每次访问CollectionView 时都会更新此日期。

视图管理器现在偶尔会在过期日期到期时清除这些引用。最后,这些参考资料将在以下时间收集 CollectionView 未被应用程序引用(由垃圾收集器确保),并且不再引用底层集合(由垃圾收集器确保)。

引入此行为是为了允许强引用R2.2,同时避免泄漏。

结论

由于未实现INotifyCollectionChanged 的集合的CollectionView 的特殊生命周期管理(使用到期日期),CollectionView 的存活时间(在内存中)要长得多。并且因为 CollectionView 通常对其源集合具有强引用,因此该集合及其项和所有可访问的引用也会保持更长时间。

如果集合实现了INotifyCollectionChanged,那么视图管理器将不会存储对CollectionView 的强引用,因此CollectionView 将在它不再被引用并且源集合的那一刻被垃圾回收变得无法访问。

重要的是,对CollectionView 的强引用的生命周期由ViewManager 即绑定引擎管理。由于管理算法(到期日期和偶尔清除),此生命周期显着延长。
因此,在对集合及其视图的所有引用都被销毁之后,观察持久分配的内存是具有欺骗性的。这不是真正的内存泄漏。

【讨论】:

    【解决方案3】:

    我使用 JustTrace 内存分析器查看了您的示例,除了一个明显的问题之外,您为什么要杀死视图模型/使 DataContext 无效并让视图继续运行(在 99.9% 的情况下,您会杀死 View 和 DataContext - 因此 ViewModel 和绑定自动超出范围)这是我发现的。

    如果您将示例修改为:

    • 将 DataContext 替换为视图模型的新实例,正如预期的那样,Person 的现有实例超出范围,因为 MS.Internal.Data.DataBingingEngine 刷新所有绑定,即使它们是不受 WeakPropertyChangedEventManager 管理的强引用,或者:
    • ViewModel 将 List 替换为 IEnumerable 的新实例,即 new Person[0]/simply null 并在 ViewModel 上引发 INCP.PropertyChanged("List")

    以上修改证明您可以安全地在绑定中使用 IEnumerable/IEnumerable。顺便说一句,Person 类也不需要实现 INPC - TypeDescriptor binding/Mode=OneTime 在这种情况下没有任何区别,我也验证了这一点。顺便说一句,对 IEnumerable/IEnumerable/IList 的绑定被包装到 EnumerableCollectionView 内部类中。不幸的是,我没有机会通过 MS.Internal/System.ComponentModel 代码找出为什么设置 DataContext = null 时 ObservableCollection 有效,可能是因为微软人员在取消订阅 CollectionChanged 时做了特殊处理。随意浪费生命中宝贵的几个小时来浏览 MS.Internal/ComponentModel :) 希望它有所帮助

    【讨论】:

      猜你喜欢
      • 2013-07-26
      • 1970-01-01
      • 1970-01-01
      • 2021-03-23
      • 2021-09-25
      • 2014-12-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多