6

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

window 标签内的 xml

<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 对象的引用。

4

3 回答 3

6

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

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

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

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

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

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

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

于 2013-10-22T07:36:18.803 回答
1

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

抽象的

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

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

以下代码触发与绑定 a 相同的行为List\<T>

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创建:它是创建和管理视图的绑定引擎。

集合和之间的关系CollectionView是一种单向依赖关系,其中CollectionView集合知道集合以便自身同步,而集合不知道集合CollectionView

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

当一个集合实现INotifyCollectionChanged

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

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

这样, 的生命CollectionView周期与集合的生命周期相耦合:即使应用程序没有对 a 的引用CollectionView,由于这种强引用,生命周期CollectionView也会延长,直到集合本身符合垃圾回收条件。
由于CollectionView实例存储在ViewTableas WeakReference W1中,因此这种生命周期耦合可防止WeakReference W1过早地收集垃圾。
换句话说,这种强耦合R1.1防止了在收集之前CollectionView被垃圾收集。

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

当一个集合没有实现时INotifyCollectionChanged

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

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

它通过将强参考R2.2存储到CollectionView. 此时,视图管理器已经存储了一个强引用R2.2 (由于CollectionView缺少INotifyCollectionChanged),而这CollectionView有一个强引用R2.1到底层集合。
这导致视图管理器保持CollectionView活动状态(R2.2),因此CollectionView使底层集合保持活动状态( R2.1):这是感知内存泄漏的原因。

但这并不是真正的泄漏,因为视图管理器通过将强引用 R2.2 注册为到期日期来控制强引用R2.2的生命周期每次访问.CollectionViewCollectionView

视图管理器现在偶尔会在它们的到期日期到期时清除这些引用。CollectionView最后,当应用程序没有引用这些引用(由垃圾收集器确保)并且不再引用底层集合(由垃圾收集器确保)时,这些引用将被收集。

引入此行为是为了在避免泄漏的同时允许强参考R2.2 。

结论

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

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

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

于 2021-11-18T21:32:51.110 回答
0

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

如果您将示例修改为:

  • 用视图模型的新实例替换 DataContext ,正如预期的那样, Person 的现有实例超出范围,因为 MS.Internal.Data.DataBingingEngine 刷新所有绑定,即使它们是不受 WeakPropertyChangedEventManager 管理的强引用,或者:
  • ViewModel 用 IEnumerable 的新实例替换 List,即 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 :) 希望它有所帮助

于 2017-11-18T23:03:56.883 回答