Dziś miało być o praktycznym przykładzie wykorzystania WeakReference ale stwierdziłem, że najpierw powrócimy znów na chwilę do WPF. Co myślicie o takiej prostej klasie:
class TimePresenterViewModel:BaseViewModel { private readonly DispatcherTimer _timer; const int RefreshTime=6*1000; public TimePresenterViewModel() { _timer=new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime); _timer.Tick += TimerTick; _timer.Start(); } void TimerTick(object sender, EventArgs e) { // jakas logika np.: OnPropertyChanged("CurrentTime"); } public string CurrentTime { get { return DateTime.Now.ToString(); } } }
Następnie gdy użytkownik naciska przycisk, usuwamy obiekt:
public partial class MainWindow : Window { private TimePresenterViewModel _timePresenterViewModel; public MainWindow() { InitializeComponent(); _timePresenterViewModel=new TimePresenterViewModel(); } private void button_Click(object sender, RoutedEventArgs e) { _timePresenterViewModel = null; } }
Wszystko wydaje się OK ale niestety DispatcherTimer spowoduje memory leak. Zajrzyjmy do profiler’a:
Nie wystarczy usunięcie wszystkich referencji. Łatwo w kodzie o taki błąd ponieważ żadna z naszych referencji nie wskazuje na timer’a a mimo wszystko jest memory leak. Spowodowane jest to wewnętrzną budową timer’a.
Rozwiązaniem jest ręczne zatrzymywanie timer’a przed wyzerowaniem wszystkich referencji.
class TimePresenterViewModel { private DispatcherTimer _timer; const int RefreshTime=6*1000; public void Init() { if(_timer!=null) return; _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(RefreshTime); _timer.Tick += TimerTick; _timer.Start(); } void TimerTick(object sender, EventArgs e) { // jakas logika np.: OnPropertyChanged("CurrentTime") } public void Release() { if(_timer!=null) { _timer.Stop(); _timer-=TimerTick; _timer=null; } } public string CurrentTime { get { return DateTime.Now.ToString(); } } }
Sposób użycia:
public partial class MainWindow : Window { private TimePresenterViewModel _timePresenterViewModel; public MainWindow() { InitializeComponent(); _timePresenterViewModel=new TimePresenterViewModel(); _timePresenterViewModel.Init(); } private void button_Click(object sender, RoutedEventArgs e) { _timePresenterViewModel.Release(); _timePresenterViewModel = null; } }
Należy zatem pilnować DispatcherTimer’a aby nie okazało się, że tracimy wszystkie referencje a nie wywołaliśmy Stop.
Ja tu bardziej widze odpięcie się od eventa niż metodę Stop() jako naprawę Memory leak-a 🙂
Nie, odpiecie nie ma znaczenia tak naprawde. Mozesz odpiac ale i tak bedzie memory leak. Na dolaczonym rysunku widac ze Start doczepia do listy watkow referencje. Dopiero Stop usuwa.
Cześć Piotrze!
Dzięki za trafne spostrzeżenie. Wiem, że ten wpis jest tu od kilku lat, ale nurtuje w mnie poniższe pytanie:
czy można klasę TimePresenterViewModel można tak napisać, aby sama wywoływała metodę Release w przypadku, gdy obiekt tej klasy jest niszczony?
– przypisany null ?
– obiekt jest przetwarzany przez Garbage Collector ?
– “jakoś” wymusić Dispose ?
Problem do rozwiązania:
======================
Jeśli wrzucę klasę TimePresenterViewModel do złożonego projektu, to inny programista za rok nie będzie wiedział, że mam wywołać metodę Release(), co jak zauważyłeś powoduje wycieki pamięci.
Pozdrawiam z nad morza / Koszalin
Grzegorz “Grzenek” Pawluch
@Grzegorz:
Nie przychodzi mi nic do glowy teraz. Wymuszanie IDisposable moglo by zadzialac, ale co jak programista zapomni wywowlac Dispose. Co prawda, CodeAnalysis wykryje to ale, nie jest to blad kompilacji.
Przeladowywanie destruktora jest zwykle zlym rozwiazaniem. W przypadku tego przykladu, nawet nie zostanie on wywolany.
Raczej bym przyjrzal sie innym timer’om. Nie pamietam juz dokladnie, ale w .NET framework jest wiele innych implementacji, moze ktoras z nich jest sprytniejsza.