WeakReference – weak event pattern

Dziś jak obiecałem, praktyczny przykład wykorzystania WeakReference ( o którym mowa była kilka postów wcześniej). Zdarzenia są częstym powodem memory leak.  Na przykład:

public class ReportViewModel
{
    public void Init(EmployeesViewModel employeesVm)
    {
        employeesVm.PropertyChanged+=EmployeesPropertyChanged;
    }
    private void EmployeesPropertyChanged(object sender,EventArgs e)
    {
        // logika
    }
}

Następnie gdzieś w kodzie tworzymy ReportViewModel, wykonujemy jakieś operację i usuwamy na końcu referencje:

ReportViewModel reports = new ReportViewModel();
reports.Init(employeesViewModel);
// jakies operacje itp.
reports = null

Załóżmy, że nie mamy więcej referencji do ReportViewModel. Mimo to, powyższy kod poskutkuje memory leak – zapomnieliśmy usunąć zdarzenie. Zdarzenie to nic innego jak wskaźnik na funkcje. Jeśli nie usuniemy tego ręcznie wtedy EmployeesViewModel.PropertyChanged będzie wskazywało wciąż na ReportViewModel.  Jeśli jeszcze nie jest to jasne, warto poczytać o wzorcu projektowym obserwator.

Wiemy, że zdarzenie także tworzy referencje, która zapobiega usunięciu obiektu przez GC. W większości przypadków najlepiej samemu sprzątać zasoby. Czasami jednak jest to niemożliwe lub bardzo trudne. Na przykład w WPF wiele kontrolek wykorzystuje WeakReference ponieważ nie wiedzą kiedy obiekt zostanie usunięty z pamięci. Przyjrzyjmy się poniższemu prostemu przykładowi (wersja zaktualizowana, w poprzedniej był poważny błąd znaleziony przez apl):

sealed class WeakEvent
{
   private readonly INotifyPropertyChanged _sourceObject;
   private readonly WeakReference _targetWeakReference;

   public WeakEvent(INotifyPropertyChanged sourceObject,
                       ViewModel target)
   {
       _sourceObject = sourceObject;
       _targetWeakReference = new WeakReference(target);
       sourceObject.PropertyChanged += SourceObjectPropertyChanged;
   }

   void SourceObjectPropertyChanged(object sender, PropertyChangedEventArgs e)
   {
       ViewModel target = (ViewModel)_targetWeakReference.Target;
       if (target != null)
           target.RaisePropertyChanged(e.PropertyName);
       else
       {
           _sourceObject.PropertyChanged -= SourceObjectPropertyChanged;
       }
   }
}
public class SampleClass:INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   public void RefreshProperty(string propName)
   {
       if(PropertyChanged!=null)
           PropertyChanged(this,new PropertyChangedEventArgs(propName));
   }
}

Powyżej został zdefiniowany wrapper na zdarzenie. Referencja do obiektu target jest przechowywana za pomocą weak reference. Dzięki temu, gdy target zostanie usunięty nie wystąpi memory leak:

public class ViewModel
{
    private readonly WeakEvent _weakEvent;

    public ViewModel(SampleClass sampleClass)
    {
        _weakEvent=new WeakEvent(sampleClass,this);
    }
   public void RaisePropertyChanged(string propName)
   {
       
   }
}
//
SampleClass sampleClass=new SampleClass();
ViewModel viewModel = new ViewModel(sampleClass);
sampleClass.RefreshProperty(null);
viewModel = null;
GC.Collect();
sampleClass.RefreshProperty(null);

Gdybyśmy nie mieli WeakReference, metoda RaisePropertyChanged zostałaby wykonana dwukrotnie. W momencie jednak, gdy zerujemy zmienną viewModel i wymuszamy posprzątanie za pomocą GC.Collect(), wywołanie RefreshProperty spowoduje wyłącznie usunięcie jednego handler’a. WeakEvent przechowuje słabe referencje więc jeśli target (obiekt docelowy) zostanie usunięty wtedy w aplikacji nie ma żadnych silnych powiązań. Powyższe rozwiązanie jest dobre kiedy trudno kontrolować samemu zdarzenia. W następnym poście pokaże bardziej generyczną klasę dla WeakReference. WeakEvent jest dość niewygodny w użyciu. W momencie kiedy nie nastąpi żadne wywołanie zdarzenia wtedy WeakEvent będzie powodował memory leak. Jest to tzw. sacrifice object i ma to tylko sens gdy prawdziwy obiekt jest dużo większy niż WeakEvent.

5 thoughts on “WeakReference – weak event pattern”

  1. “Wiemy, że zdarzenie także tworzy referencje, która zapobiega usunięciu obiektu przez GC.”
    Rozumiem, że utworzy nam się swego rodzaju wysepka w pamięci. Ale czy GC nie poradzi sobie z tym problemem na etapie ‘Mark and Swype’?
    Jestem świadomy, że Mark and Swype jest bardziej żerne (wymaga zatrzymania wątku aplikacji) i uruchamia się tylko po przekroczeniu pewnej ilości zajętej pamięci.
    Żeby nie było wątpliwości – jestem dość świeżym dotnetowcem i absolutnie nie kwestionuje zasadności użycia Weak Reference – pytam jedynie, jak Mark and Swype poradzi sobie z opisanym problemem.

    Pozdrawiam

  2. Słyszałem wcześniej o tego rodzaju wyciekach pamięci, ale dopiero teraz dowiedziałem się jak im przeciwdziałać.

    Wymaga to, jak widać, stosunkowo dużo więcej kodu, ale jak rozumiem, to jedyne wyjście, aby zapobiegać takim wyciekom? Czy też może istnieje jakiś jeszcze inny sposób?
    Pewnie w najprostszych przypadkach dałoby radę po prostu użyć odpięcia handlera od eventu, ale jak napisałeś – nie wszędzie tak się da.

    Dzięki za kolejną porcję wiedzy – przy okazji – skąd ją bierzesz? Jeżeli to nie tajemnica oczywiście 🙂

  3. @Pawel:
    Jesli tylko da sie, lepiej samemu odpiac niepotrzebne zdarzenie. Ale nalezy zaznaczyc, ze nie zawsze trzeba pamietac o tym. Jesli sie podpinamy do zdarzenia klasy ktora jest wewnatrz naszej instancji wtedy nie ma potrzeby. Problem jest wylacznie wtedy gdy np. z short-live object podpisany zdarzenie do long-live object.
    A problemy opisuje, ktore codziennie trzeba m.in. w pracy rozwiazywac:)
    @Jacek:
    Nie poradzi bo zdarzenie to po prostu silna referencja. A poki istnieje chocby jedna silna referencja wtedy GC nie ma prawa usunac obiektu.

  4. A nie będzie tak, że pierwsze odśmiecenie pamięci spowoduje usunięcie handlera nawet jeśli instancja ViewModel nadal istnieje i jest w użyciu? W końcu referuje do niego wyłącznie WeakReference.

    SampleClass sampleClass = new SampleClass();
    ViewModel viewModel = new ViewModel(sampleClass);

    sampleClass.RefreshProperty(null);

    GC.Collect();
    GC.WaitForPendingFinalizers();

    sampleClass.RefreshProperty(null);

    GC.KeepAlive(viewModel);

  5. @apl
    Masz racje. Poprzednia implementacja była zdecydowanie niepoprawna. Zaaktualizowalem (dodalem warapper, patrz wyzej), teraz nie powinno byc tego bledu. W nastpenym poscie pokaze lepsza implementacje bo ta ma rowniez wady i nie jest zbyt elastyczna.
    Wielkie dzieki.

Leave a Reply

Your email address will not be published.