In settimana mi sono occupato di un problema di memory leak (o forse meglio dire objects leak) dal quale ne è nata una segnazione per MS di cui stiamo attendendo risposta. Mi è sembrata un esperienza interessante che mi fa piacere raccontare, vediamo se riesco ;-p
Il punto di partenza è che si registrano problemi di risorse dopo un "lungo" utilizzo di un grossa applicazione Windows Form Fx 1.1; in particolare ad una prima analisi con il task manager si è notato uno strano incremento degli oggetti GDI usati. Quando gli oggetti GDI raggiungono un numero limite ovviamente il programma diventa inusabile. Nella giornata abbiamo usato ovviamente anche altri strumenti di analisi quali CLR Profiler (v1.1) e .NET Memory Profiler. Nel mio racconto userò solo CLR Profiler (v1.1) per mostrare i grafi dello Heap. Nel corso della giornata siamo riusciti ad individuare un possibile problema e a riprodurre una demo per presentarlo... che userò in questa mia storia.
Consideriamo questo modello di oggetti.
public class Person
{
private string name;
public string Name
{
get{return name;}
set{name = value;}
}
private Gender gender;
public Gender Gender
{
get{return this.gender;}
set{this.gender = value;}
}
}
public class Gender
{
private string description;
public string Description
{
get{return description;}
set{description = value;}
}
}
Implementiamo poi un form (Form1) che mette in bind una lista di oggetti Person; segue qui sotto la procedura con cui facciamo il bind della lista con la form.
//[Form1]
public void SetDataSource(IList datasource)
{
this.datasource = datasource;
Binding bnd_name = new Binding ("Text", datasource, "Name");
textBox1.DataBindings.Add(bnd_name);
BindingContext[datasource].PositionChanged+=new EventHandler(Form1_PositionChanged);
BindingContext[datasource].Position = 0;
}
La nostra applicazione demo comprende anche una form MainForm con la quale è possibile aprire le varie form rappresentati i vari casi di utilizzo. Ecco qui quindi un primo screenshot fatto allo startup dell'applicazione: in memoria vediamo un oggetto MainForm e abbiamo 41 oggetti GDI usati.
Apro la Form1 e controllo la situazione... in memoria c'è un oggetto di tipo Form1 e gli oggetti GDI sono aumentati (da 41 a 47).
Chiudo Form1 e controllo nuovamente la situazione: non ci sono più oggetti di tipo Form1 in memoria e dopo aver invocato esplicitamente GC.Collect() gli oggetti GDI tornano allo stato iniziale (41).
Prepariamo ora un'altra form (Form2) con una piccola variante per quanto riguarda il binding come qui sotto riportato. Il particolare della variante/aggiunta è il binding su proprietà di oggetti derivanti da altre proprietà. Indagando con reflection si scopre che tali tipi di binding dovrebbero essere gestiti dalla classe interna "System.Windows.Forms.RelatedPropertyManager".
//[Form2]
public void SetDataSource(IList datasource)
{
this.datasource = datasource;
Binding bnd_name = new Binding ("Text", datasource, "Name");
textBox1.DataBindings.Add(bnd_name);
Binding bnd_gender = new Binding ("Text", datasource, "Gender.Description");
textBox2.DataBindings.Add(bnd_gender);
BindingContext[datasource].PositionChanged+=new EventHandler(Form2_PositionChanged);
BindingContext[datasource].Position = 0;
}
Come per la Form1 apro la Form2 e controllo lo stato delle cose... e come prima vediamo apparire un oggetto Form2 nel grafo degli oggetti dello heap e aumentano gli oggetti GDI.
Chiudo Form2, chiamo esplicitamente GC.Collect() e controllo lo stato delle cose.
...gli oggetti GDI NON sono tornati allo stato iniziale (43 invece di 41) e infatti nello Heap è rimasta appesa un istanza di Form2 tenuta in vita da EventHandler di un CurrencyManager!
Aprendo e chiudendo ripetutamente la Form2 si assiste ad un progressivo aumento di istanze di Form2 appese e di conseguenza degli oggetti GDI... ovviamente un aumento piccolo perchè nel nostro esempio stiamo usando una form piccola.
Per un possibile workaround al problema è stato pensata una rimozione esplicita del binding... da applicare poi in modo intelligente nel progetto reale sfruttando - ad esempio - gli eventi di Close o Dipose del form.
private void button3_Click(object sender, System.EventArgs e)
{
BindingContext[datasource].PositionChanged-=new EventHandler(Form2_PositionChanged);
foreach(Control c in this.Controls)
{
c.DataBindings.Clear();
}
}
La storia è finita. Per chi fosse interessato al problema resti sintonizzato per avere informazioni sull'eventuale risposta di MS o per altre novità.
oO0(...ad esempio non ho provato la demo sul Fx 2.0; in questi giorni farò delle prove e ne riporterò l'esito...)
posted @ domenica 18 febbraio 2007 23:33