9

所以我使用带有 MVVM + DataTemplate 方法的 WPF 3.5 在 GUI 上加载 2 个视图。我在内存分析时观察到,作为项目控件的项目容器的一部分生成的项目被固定到内存中,即使在视图被卸载后也不会被 GC!

我刚刚进行了测试,发现即使是最简单的代码也可以重现它......你们可以自己检查。

XAML:

<Window x:Class="ContentControlVMTest.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ContentControlVMTest"
        Title="Window2" Height="300" Width="300">
    <DockPanel LastChildFill="True">

        <CheckBox Click="CheckBox_Click" Content="Test1?"
                  DockPanel.Dock="Top" Margin="5"/>

        <ContentControl x:Name="contentControl">
            <ContentControl.Resources>

                <DataTemplate DataType="{x:Type local:Test3}">
                    <TextBlock Text="{Binding C}" Margin="5"/>
                </DataTemplate>

                <DataTemplate DataType="{x:Type local:Test1}">
                    <DockPanel LastChildFill="True" Margin="5">
                        <TextBlock Text="{Binding A}"
                                   DockPanel.Dock="Top"
                                   Margin="5"/>
                        <ListBox ItemsSource="{Binding Bs}"
                                 DisplayMemberPath="B"
                                 Margin="5"/>
                    </DockPanel>
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>
    </DockPanel>
</Window>

代码背后:

public class Test3
{
    public string C { get; set; }
}

public class Test2
{
    public string B { get; set; }
}

public class Test1
{
    public string A { get; set; }

    private List<Test2> _Bs;
    public List<Test2> Bs
    {
        get
        {
            return _Bs;
        }

        set
        {
            _Bs = value;
        }
    }
}

public partial class Window2 : Window
{
    public Window2()
    {
        InitializeComponent();
        this.KeyDown += Window_KeyDown;
    }

    private void Window_KeyDown
            (object sender, System.Windows.Input.KeyEventArgs e)
    {
        if (Keyboard.IsKeyDown(Key.LeftCtrl))
            if (Keyboard.IsKeyDown(Key.LeftShift))
                if (Keyboard.IsKeyDown(Key.LeftAlt))
                    if (Keyboard.IsKeyDown(Key.G))
                    {
                        GC.Collect(2, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(2, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(3, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(3, GCCollectionMode.Forced);
                    }
    }

    private void CheckBox_Click(object sender, RoutedEventArgs e)
    {
        if (((CheckBox)sender).IsChecked.GetValueOrDefault(false))
        {
            var x = new Test1() { A = "Test1 A" };
            x.Bs = new List<Test2>();
            for (int i = 1; i < 10000; i++ )
            {
                x.Bs.Add(new Test2() { B = "Test1 B " + i });
            }
            contentControl.Content = x;
        }
        else
        {
            contentControl.Content = new Test3() { C = "Test3 C" };
        }
    }
}

我通过 Left Shift + Alt + Ctrl + G 执行强制 GC。or 视图和 View Model 的所有项目在Test1正确Test3卸载后都会失效。所以这正如预期的那样。

但是模型中生成的集合Test1(具有Test2对象)仍然固定在内存中。它表明该数组是列表框的项目容器使用的数组,因为它显示了列表框中的去虚拟化项目的数量!当我们在视图模式下最小化或恢复视图时,这个固定数组会改变它的大小Test1!一次是 16 项,下一次是 69 项。

在此处输入图像描述

这意味着 WPF 会固定在项目控件中生成的项目!谁能解释一下?这有什么明显的缺点吗?

多谢。

4

2 回答 2

3

该问题是由于绑定机制未能完全释放实际绑定的列表项以在屏幕上显示造成的。最后一点几乎肯定是您在不同运行中看到不同数量的“孤立”实例的原因。滚动列表越多,产生的问题就越多。

这似乎与我一年多前提交的错误报告中描述的相同类型的潜在问题有关,因为固定根和固定实例树是相似的。(要以方便的格式查看这种详细信息,您可能需要获取一个更高级的内存分析器的副本,例如ANTS Memory Profiler。)

真正的坏消息是你的孤立实例被固定在窗口本身的消亡之后,所以如果没有我在 WinForms 场景中必须使用的相同类型的黑客来强制清理它们,你可能无法清理它们。绑定私人。

所有这一切中唯一的好消息是,如果您可以避免绑定到嵌套属性,则不会出现问题。例如,如果您将 ToString() 覆盖添加到 Test2 以返回其 B 属性的值并从您的列表框项中删除 DisplayMemberPath,那么问题就会消失。例如:

public class Test2
{
    public string B { get; set; }

    public override string ToString()
    {
        return this.B;
    }
}

<ListBox ItemsSource="{Binding Bs}" 
    Margin="5"/>
于 2012-02-24T20:19:59.077 回答
0

在您上面的示例代码中,我看不到您在哪里卸载任何视觉效果?

但是假设您正在卸载整个视图,这仍然是可以预测的。您没有考虑的因素是调度程序。Dispatcher 是一个优先事件队列,对于该队列中的每个委托,它维护对这些委托指向的对象的引用。这意味着在 Unloaded 事件之后,您认为某些东西很有可能在队列中,因此在 GC 中具有合法引用。你可以 GC.Collect 直到你脸色发青,它永远不会收集带有剩余引用的对象。

所以你要做的就是给dispatcher打气,然后调用GC.Collect。像这样的东西:

void Control_Unloaded(object sender, RoutedEventArgs e)
{
  // flush dispatcher
  this.Dispatcher.BeginInvoke(new Action(DoMemoryAnalysis), DispatcherPriority.ContextIdle);
}

private static void DoMemoryAnalysis()
{
  GC.Collect();
  GC.WaitForPendingFinalizers();

  // do memory analysis now.
}

.net 中内存泄漏的另一个真正常见的原因与附加事件有关,而不是正确地取消附加事件。我在上面的示例中没有看到您这样做,但如果您要附加事件,请确保您在 Unloaded 或 Dispose 或任何最合适的地方取消附加它们。

于 2012-02-23T18:04:18.720 回答