Silverlight + MVVM + Bindings = Memory leaks?

2019-02-09 06:23发布

问题:

Thus far, my testing has shown that all standard approaches, examples, and frameworks leveraging the MVVM pattern in silverlight suffer from a huge problem: massive memory leaks which prevent VMs from being garbage collected.

Obviously this is a huge and ridiculous claim - so my expectation is that someone will have an obvious answer of why and where I'm going wrong :)

The steps to reproduce are simple:

  • Bind your viewmodel to a view by setting the views datacontext to the VM (assume the viewmodel leverages INotifyPropertyChanged to support data binding)
  • Bind a UI element to a property on the viewmodel, for example:

<TextBox Text="{Binding SomeText}" />

  • Leverage the binding in some way (for example - just type in the textbox).

This creates a reference chain that extends from the root, to a BindingExpression, to your viewmodel. You can then remove the View from your UI tree, as well as all refernences to the VM - however the VM will never be garbage collected thanks to the root<>BindingExpression<>VM reference chain.

I have created two examples illustrating the problem. They have a button to create a new view/viewmodel (which should dump all references to the old one(s)) and a button which forces garbage collection and reports on the current memory usage.

Example 1 is a super stripped down caliburn micro example. Example 2 uses no frameworks and just illustrates the problem in the simplest way I could think of.

Example 1

Example 2

For those who might like to help but don't wish to download the example projects, here is the code for example 2. We start with a viewmodel called FooViewModel:

 public class FooViewModel : INotifyPropertyChanged
{
    string _fooText;

    public string FooText
    {
        get { return _fooText; }
        set
        {
            _fooText = value;
            NotifyPropertyChanged("FooText");
        }
    }

    private byte[] _data;
    public FooViewModel()
    {
        _data = new byte[10485760]; //use up 10mb of memory
    }



    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

It simply exposes a string property called FooText which we will bind too. INotifyPropertyChanged is neccessary to facilitate the binding.

Then we have a view called FooView which is a usercontrol containing:

<UserControl x:Class="MVVMLeak.FooView">
    <StackPanel x:Name="LayoutRoot" Orientation="Horizontal">
        <TextBlock Text="Bound textbox: " />
        <TextBox Text="{Binding FooText}" Width="100"/>
    </StackPanel>
</UserControl>

(namespaces omitted for brevity)

The important bit here is the textbox which is bound to the FooText property. Of course we need to set the datacontext, which I've chosen to do in the codebehind rather than introduce a ViewModelLocator:

public partial class FooView : UserControl
{
    public FooView()
    {
        InitializeComponent();
        this.DataContext = new FooViewModel();
    }
}

MainPage looks like this:

<StackPanel x:Name="LayoutRoot" Background="White">

    <Button Click="Button_Click" Content="Click for new FooView"/>
    <Button Click="Button2_Click" Content="Click to garbage collect"/>
    <ContentControl x:Name="myContent"></ContentControl>
</StackPanel>

with the following in the code behind:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        myContent.Content = new FooView();
    }

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Memory in use after collection: " + (GC.GetTotalMemory(true) / 1024 / 1024).ToString() + "MB");
    }

Note: To replicate the problem, be sure to type something in the textbox, as I believe the Binding Expression isn't created until it's needed.

It's worth noting that this KB article may be related, however I'm not convinced since 'method 2' workaround doesn't seem to have an effect, and the reference chain doesn't seem to match.

Also, I'm not sure it matters, but I used CLR Profiler to diagnose the cause.

Update:

If anyone would like to test, and report their findings in a comment, I'm hosting the silverlight application via dropbox here: Hosted Example . To reproduce: Hit the top botton, type something, hit the top button, type something, hit the top button. Then hit the button. If it reports 10MB usage (or perhaps some other amount that is not increasing), you are not experiencing the memory leak.

Thus far, the problem seems to be happening on ALL of our development machines, which are ThinkPad w510 (43192RU) with 12GB Ram, 64 bit Win 7 Enterprise. This includes some which have not had development tools installed. It might be worth noting that they are running VMWare workstation.

The problem does NOT seem to happen on other machines I have tried - including several home PCs and other PCs in the office. We have somewhat ruled out SL versions, amount of memory, and probably vmware. Still haven't nailed down a cause.

回答1:

A solution is yet to be found, however the problem is now identified. This behavior will occur if Silverlights' automation faculties are invoked due to:

  • Tablet PC Input Service (in other words, all 'tablet like' PCs)
  • Automated Testing tools
  • Screen Readers (and other accessability software)

More information here: http://www.wintellect.com/cs/blogs/sloscialo/archive/2011/04/13/silverlight-memory-leaks-and-automationpeers.aspx

So a new problem surfaces: How do we disable automationpeers or otherwise get them to clean-up correctly?

This post illustrates one approach: WPF UserControl Memory leak

However, it isn't really a viable solution as we'd have to override every silverlight control which we plan to use binding for, not to mention the control templates of complex controls.

I will change my answer if anyone can identify a good solution, but for now there doesn't seem to be one...

Edit:

Here is a nice little workaround which seems to do the job. Simply add the following parameter in your HTML where you define the silverlight object:

<param name="windowless" value="true" />

A side-effect of running in 'windowless' mode is that automation doesn't work :)



回答2:

There is no memory leak in your second example.

After you affect a new FooView instance to your ContentControl using myContent.Content = new FooView();, there is no more used reference to the whole View + ViewModel object graph.

It'll be garbage-collected when needed, eventually.

Maybe you should clarify about what make you think there is a memory leak (i.e. statistics, repro steps...etc.)