2012-02-21 15 views
9

Así que estoy usando WPF 3.5 con el método MVVM + DataTemplate para cargar 2 vistas en la GUI. ¡He observado mientras que el perfil de la memoria que los elementos generados como parte del contenedor de artículos de los controles de artículos se anclan en la memoria y no se acumulan GC incluso después de que se descargue la vista!Instancias anotadas para GC - No detectable desde mi código administrado

Acabo de ejecutar pruebas y descubrí que es reproducible incluso para el código más simple ... Ustedes pueden comprobarlo por sí mismos.

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> 

código subyacente:

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

llevo a cabo GC obligado por desplazamiento a la izquierda + Alt + Ctrl + G. Todos los artículos para la vista Test1 o Test3 y Ver modelo se muere después de que se descargan correctamente. Entonces eso es lo esperado.

Pero la colección generada en el modelo Test1 (que tiene objetos Test2), permanece anclada en la memoria. ¡Y indica que la matriz es la utilizada por el contenedor de elementos del listbox porque muestra la cantidad de elementos desvirtualizados del listbox! ¡Esta matriz anclada cambia su tamaño cuando minimizamos o restauramos la vista en el modo de vista Test1! Una vez fueron 16 ítems y la próxima vez fue 69 ítems cuando se perfilaron.

enter image description here

Esto significa WPF realiza la fijación de elementos generados en artículos controles! ¿Alguien puede explicar esto? ¿Esto tiene algún inconveniente significativo?

Thx mucho.

+0

Tal vez un 'CollectionView' creado para la colección está dando vueltas. – user7116

+0

thx para la respuesta. ¡Sí! Es la colección de elementos del contenedor de artículos. Pero ¿por qué sería eso dando vueltas? La vista se ha ido. ListBox se ha ido. ¿Por qué WPF fijaría colecciones en la memeoría? –

+0

Podría publicar la raíz para la instancia fijada por favor. –

Respuesta

3

El problema se debe a que el mecanismo de encuadernación no ha liberado completamente los elementos de la lista que se han visto obligados a mostrarse en la pantalla. Es casi seguro que ese último bit sea el motivo por el que está viendo diferentes números de instancias "huérfanas" en diferentes ejecuciones. Cuanto más se desplaza por la lista, más problemas genera.

Esto parece estar relacionado con el mismo tipo de problema subyacente que se describe en a bug report that I submitted over a year ago, ya que la raíz fija y los árboles de instancia anclados son similares. (Para ver ese tipo de detalles en un formato conveniente, es posible que desee obtener una copia de un perfilador de memoria un poco más elegante como ANTS Memory Profiler.)

La mala noticia es que sus instancias huérfanas están siendo fijadas más allá de la desaparición de la la ventana misma, por lo que probablemente no pueda limpiarlos sin el mismo tipo de truco que tuve que usar en el escenario de WinForms para forzar la limpieza de las partes privadas de unión.

La única buena noticia en todo esto es que el problema no ocurre si puede evitar el enlace a propiedades anidadas. Por ejemplo, si agrega una anulación de ToString() a Test2 para devolver el valor de su propiedad B y elimina DisplayMemberPath de su elemento de cuadro de lista, el problema desaparecerá. ej .:

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

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

<ListBox ItemsSource="{Binding Bs}" 
    Margin="5"/> 
+1

Ah sí, el notorio problema de fuga vinculante. Esto también es cierto, lo he visto. Aunque puede solucionarlo implementando INotifyPropertyChanged en su modelo. Incluso si nunca llama evento PropertyChanged, su mera presencia soluciona el problema en el marco. También en el enlace puede agregar Mode = OneWay o Mode = OneTime para solucionar el problema de fuga de enlace. Aunque esto no siempre está disponible. Pruebe ambos, agregue INotifyPropertyChanged y {Binding B, Mode = OneWay}. –

+0

@Nicole & Justin, Thx mucho por su ayuda. El uso de ToString() o OneTime Binding corrigió las instancias huérfanas de Test2. Thx nuevamente por eso. Pero una sola instancia de la matriz Test2 [] aún permanece anclada y ocupa 16 bytes de memoria. Aunque esto es mucho mejor que tener un número "n" de instancias de Test2 anclado en la memoria, pero ¿cómo eliminarlo? Intenté 'INotifyPropertyChanged' para la colección' Bs' también (también cansado el enlace OneTime de 'Bs') ... la matriz aún permanece. ¿Alguna ayuda aquí? –

+0

@ justin.m.chase: Gracias por mencionar la solución alternativa INotifyPropertyChanged.No funcionó para mis problemas anteriores de WinForms, así que había olvidado por completo que me había topado con mencionarlo para WPF. –

0

En el código de ejemplo anterior, ¿no veo dónde está descargando alguno de los elementos visuales?

Pero suponiendo que está descargando toda la vista, esto todavía es predecible. El factor que no está teniendo en cuenta es el despachador. El despachador es una cola de eventos priorizados y para cada delegado en esa cola mantiene una referencia a los objetos apuntados por esos delegados. Lo que significa que es muy posible que algo en su vista esté en la cola después de un evento descargado y, por lo tanto, tenga una referencia legítima en el GC. Puede GC. Recoger hasta que esté azul en la cara y nunca recogerá objetos con las referencias restantes.

Entonces lo que tienes que hacer es bombear el despachador y luego llamar a GC.Collect. Algo como esto:

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

Otra causa muy común de pérdidas de memoria en .NET tiene que ver con la fijación de los acontecimientos y no unattaching correctamente. No veo que hagas esto en tu muestra anterior, pero si adjuntas eventos, asegúrate de que los estás desatando en Descargado o Disposición o donde sea más apropiado.

+0

El modelo MVVM + DataTemplate no contiene ninguna vista, vista de modelo o modelo de objetos en memeory porque la vista se sobrescribe cuando un modelo de vista de destino diferente Está establecido. ¡Mi instantánea muestra que la vista 'Test1' no está activada! Así que eso responde a su primera consulta ... Además, no tengo ningún controlador de eventos con pérdida ya que la raíz de las instancias ** Fijadas ** es imposible de rastrear (a cualquier controlador de eventos) por el generador de perfiles. Como se ve en mi código, realmente no tengo ningún evento manejado (a menos que WPF mantenga la referencia viva por alguna razón). Además, las instancias de ** Fijación ** siempre son deliberadas y mi p. no tiene ningún código de ese tipo. –

+0

WPF colocará su visual en la cola del despachador para todo tipo de eventos. No tiene que conectar eventos en su código para que esto suceda. Tuve exactamente el mismo problema cuando trabajaba en el importador de PSD en Expression Blend. Abrimos un archivo psd grande, cerramos el cuadro de diálogo y lo abrimos de nuevo y bloqueamos con la excepción de falta de memoria. ¿Cómo podría estar sin memoria si la ventana no está abierta? Es porque la ventana permanece en la cola del despachador durante bastante tiempo. Debes eliminar la cola y el GC aunque la imagen ya no esté en el árbol visual. Intentalo. –

Cuestiones relacionadas