wpf 绑定到列表导致内存泄漏

sbdsn5lh  于 2023-04-13  发布在  其他
关注(0)|答案(3)|浏览(317)

当我将ListBox的ItemsSource绑定到List时,绑定引擎会在控件消失后保留列表元素。这会导致所有列表元素都留在内存中。使用ObservalbleCollection时,问题就消失了。为什么会发生这种情况?
窗口标记内的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; }
}

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

swvgeqrz

swvgeqrz1#

啊,我明白你的意思了。
您将Content设置为null,因此您杀死了完整的ListBox,但ItemsSource仍然绑定到List,因此ListBox内存没有完全释放。
不幸的是,这是一个众所周知的问题,在MSDN上也有很好的记录。
如果未绑定到DependencyProperty或实现INotifyPropertyChanged或ObservableCollection的对象,则绑定可能会泄漏内存,并且在完成后必须取消绑定。
这是因为,如果对象不是DependencyProperty或不实现INotifyPropertyChanged或不实现INotifyCollectionChanged(正常列表不实现此操作),则它将通过PropertyDescriptor AddValueChanged方法使用ValueChanged事件。这将导致CLR创建从PropertyDescriptor到对象的强引用,并且在大多数情况下,CLR将在全局表中保留对PropertyDescriptor的引用。
因为绑定必须继续侦听更改。当目标仍在使用时,此行为使PropertyDescriptor和对象之间的引用保持活动状态。这可能导致对象和该对象引用的任何对象中的内存泄漏。
问题是... Person是否实现了INotifyPropertyChanged?

hi3rlvi2

hi3rlvi22#

这是一个老帖子了,我明白了。但是这些解释,特别是那些被接受的答案,并不是很准确,而且其含义是错误的。

摘要

在此之前,这并不是真实的的内存泄漏。特殊的绑定引擎对未实现INotifyCollectionChanged的集合及其关联的CollectionView的生存期管理会适当地照顾分配的内存。
WPF支持绑定到许多不同的类型,如DataTable和XML,或者通常绑定到实现IListIEnumerableIListSource的类型。如果这是一个严重的错误,那么所有这些绑定都是危险的。
微软会在他们的文档中传播警告,例如,绑定到DataTable,就像他们在事件或数据绑定的上下文中潜在的内存泄漏一样。
确实,当绑定到INotifyCollectionChanged类型的集合时,可以避免这种特殊行为-或者通过避免为未实现INotifyCollectionChanged的集合创建CollectionView
所观察到的行为实际上是由绑定引擎的实际X1 M10 N1 X管理而不是数据绑定本身引起的。
下面的代码触发与绑定到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
否则,绑定引擎将创建一个指向源的static引用。静态引用是根引用。由于它们在应用程序的生存期内可以访问,因此此类根引用(如静态字段和它们引用的每个对象(内存))将永远不符合垃圾回收的条件,从而导致内存泄漏。

收藏和CollectionView管理

集合则是另一回事。导致alledged泄漏的原因不是数据绑定本身。而是绑定引擎,它也负责创建实际集合的CollectionView
CollectionView是在绑定的上下文中创建的,还是在调用CollectionViewSource.GetDefaultView时创建的:它是创建和管理视图的绑定引擎。
collection和CollectionView之间的关系是单向依赖关系,其中CollectionView知道集合以便同步自身,而集合不知道CollectionView
每个现有的CollectionView都由ViewManager管理,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示例作为WeakReferenceW1 存储在ViewTable中,因此这种生命周期耦合可以防止WeakReferenceW1 过早地被垃圾收集。
换句话说,这种强耦合 R1.1 防止CollectionView在 * 收集之前 * 被垃圾收集。
此外,管理器还必须保证只要CollectionView被应用程序引用,底层集合就继续存在,即使该集合不再被引用。这是通过保持从CollectionView到源集合的强引用 R1.2 来实现的。
无论集合类型如何,此引用始终存在。

集合不实现INotifyCollectionChanged

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

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

它通过存储对CollectionView的强引用 R2.2 来实现这一点。此时视图管理器已经存储了对CollectionView的强引用 R2.2(由于缺少INotifyCollectionChanged),而这个CollectionView具有对底层集合的强引用 R2.1
这导致视图管理器保持CollectionView活动(R2.2),因此CollectionView保持底层集合活动(R2.1):这是导致 * 感知到的 * 内存泄漏的原因。
但这并不是真实的的泄漏,因为视图管理器通过将强引用 * R2.2 * 注册到到期日期来控制CollectionView的强引用 * R2.2* 的生命周期。该日期在每次访问CollectionView时更新。
视图管理器现在会在过期时偶尔清除这些引用。最后,当CollectionView没有被应用程序引用(由垃圾收集器确保)并且底层集合不再被引用(由垃圾收集器确保)时,这些引用将被收集。
引入此行为是为了允许强引用 R2.2,同时避免泄漏。

总结

由于CollectionView的特殊生存期管理(使用过期日期),CollectionView(在内存中)可以更长时间地保持活动状态。并且由于CollectionView通常具有对其源集合的强引用,因此该集合及其项和所有可到达的引用也可以更长时间地保持活动状态。
如果集合已经实现了INotifyCollectionChanged,那么视图管理器将不会存储对CollectionView的强引用,因此当CollectionView不再被引用并且源集合变得不可访问时,它将被垃圾收集。
重要的一点是,对CollectionView的强引用的生命周期由ViewManager即绑定引擎管理。由于管理算法(到期日期和偶尔的清除),该生命周期显着延长。
因此,在对集合及其视图的所有引用都被销毁之后,对持久化分配内存的观察是欺骗性的,它不是真实的的内存泄漏。

ne5o7dgx

ne5o7dgx3#

我用JustTrace内存分析器看了你的例子,除了一个明显的问题,为什么你要杀死视图模型/无效DataContext并让视图运行(在99.9%的情况下,你会杀死视图和DataContext -因此ViewModel和Bindings自动退出范围),下面是我的发现。
如果将示例修改为:

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

以上修改证明了可以安全的使用IEnumerable/IEnumerable进行绑定,顺便说一句,Person类也不需要实现INPC-- TypeDescriptor binding/Mode=OneTime在这种情况下没有什么区别,我也验证过了,顺便说一句,IEnumerable/IList的绑定被 Package 到EnumerableCollectionView内部类中,可惜我没有;我没有机会仔细阅读MS.Internal/System.ComponentModel的代码来找出为什么ObservableCollection在设置DataContext = null时可以工作,可能是因为微软的人在取消订阅CollectionChanged时做了一个特殊的处理。请随意浪费一些宝贵的生命周期时间来阅读MS.Internal/ComponentModel:)希望它能有所帮助

相关问题