Binding to list causes memory leak

2019-01-24 14:43发布

问题:

When I bind an ItemsSource of a ListBox to a List the binding engine holds on to the list elements after the control is gone. This causes all the list elements to stay in memory. The problem goes away when using an ObservalbleCollection. Why does this happen?

The xaml inside the window tag

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

Code behind:

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

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

ViewModel

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; }
}

Edit: To check the leaking of the person istances, I used ANTS and .Net memory profiler. Both show that after pushing the GC-button only the binding engine is holding reference to the person objects.

回答1:

Ahhh got you. Now I understand what you mean.

You set the Content to null and so you kill the compelte ListBox but still the ItemsSource binds to List and so ListBox memory is not completely released.

That is unfortunately a well known issue and also well documented on MSDN.

If you are not binding to a DependencyProperty or a object that implements INotifyPropertyChanged or ObservableCollection then the binding can leak memory, and you will have to unbind when you are done.

This is because if the object is not a DependencyProperty or does not implement INotifyPropertyChanged or not implementing INotifyCollectionChanged (Normal list is not implementing this) then it uses the ValueChanged event via the PropertyDescriptors AddValueChanged method. This causes the CLR to create a strong reference from the PropertyDescriptor to the object and in most cases the CLR will keep a reference to the PropertyDescriptor in a global table.

Because the binding must continue to listen for changes. This behavior keeps the reference alive between the PropertyDescriptor and the object as the target remains in use. This can cause a memory leak in the object and any object to which the object refers.

The question is...is Person implementing INotifyPropertyChanged?



回答2:

I had a look at your example with JustTrace memory profiler and apart from an obvious question why would you kill view model / nullify DataContext and leave view running (in 99.9% of cases you'd kill View and DataContext - hence ViewModel and Bindings go of of scope automatically) here's what I found.

It will work fine if you modify your example to:

  • replace DataContext with new instance of view model, as expected, existing instances of Person go out of scope as MS.Internal.Data.DataBingingEngine flushes all bindings, even they were strong refs not managed by WeakPropertyChangedEventManager , or:
  • ViewModel to replace List with new instance of IEnumerable i.e. new Person[0]/simply null and raise INCP.PropertyChanged("List") on the ViewModel

Above modifications prove you can safely use IEnumerable/IEnumerable in binding. BTW, Person class doesn't need to implement INPC neither - TypeDescriptor binding/Mode=OneTime don't make any difference in this case, I verified that too. BTW, bindings to IEnumerable/IEnumerable/IList are wrapped into EnumerableCollectionView internal class. Unfortunatelly, I didn;t have a chance to go through MS.Internal/System.ComponentModel code to find out why ObservableCollection works when setting DataContext = null, probably because Microsoft guys did a special handing when unsubscribing from CollectionChanged. Feel free to waste few precious lifetime hours on going through MS.Internal/ComponentModel :) Hope It helps