Pull to refresh

Избавляемся от утечек памяти в WPF

Developer Soft corporate blog
imageВ DevExpress мы тратим много сил на бизнес компоненты для WPF и Silverlight. У нас есть своя линейка контролов, в список которых недавно вошел DXPivotGrid – замена инструмента PivotTable из Excel. В процессе разработки новых компонентов, мы стараемся по-максимуму использовать существующий код. Например, базовые классы от версии PivotGrid для WinForms. Часто это рождает проблемы, с которыми ты не сталкивался, разрабатывая под .NET 2.0. Когда я писал PivotGrid для WPF, мне пришлось решить проблемы с утечками памяти из-за подписки (точнее, «неотписки») на события.


Компания Microsoft ввела понятие weak event'а в .NET 3.0: это вариация стандартных событий, которые не держат прямую ссылку на обработчик события. С другой стороны, обычные обработчики – это две ссылки: одна на объект, а вторая – на метод внутри объекта. Ничего нового, но есть нюанс.

Обработчик события не будет обработан сборщиком мусора, пока он не отпишется от события. Учитывая, что в WPF не применяется интерфейс IDisposable, это превращается в большую проблему.

Как решение, Microsoft предлагает слабо-связанные обработчики событий (weak events — «обработчики слабых событий» в переводе Microsoft). Сборщик мусора может обработать объекты, подписывающиеся на такие события, даже если подписка все ещё существует.

Есть два способа сделать слабое событие: использовать WeakEventManager или RoutedEvent.

WeakEventManager


Класс WeakEventManager позволяет превратить существующее событие в слабое событие. В моем проекте это было нужно для подписки на события из ядра продукта, которое должно быть совместимо с .NET 2.0.

Слабые события создаются при помощи двух классов: WeakEventManager (диспетчер) и WeakEventListener (прослушиватель). Диспетчер события подписывается на него и передает вызовы прослушивателям, т.е. является прослойкой между источником и получателем, разрывая жесткую связь.

Это шаблон диспетчера событий.

public class MyEventManager : WeakEventManager {
    static MyEventManager CurrentManager {
        get {
            // Создание статического диспетчера событий
            Type managerType = typeof(MyEventManager);
            MyEventManager currentManager =
                (MyEventManager)WeakEventManager.GetCurrentManager(managerType);
            if(currentManager == null) {
                currentManager = new MyEventManager();
                WeakEventManager.SetCurrentManager(managerType, currentManager);
            }
            return currentManager;
        }
    }

    MyEventManager() { }

    // Измените "T" на действительный тип источника событий
    public static void AddListener(T source, IWeakEventListener listener) {
        CurrentManager.ProtectedAddListener(source, listener);
    }
    // Измените "T" на действительный тип источника событий
    public static void RemoveListener(T source, IWeakEventListener listener) {
        CurrentManager.ProtectedRemoveListener(source, listener);
    }
    protected override void StartListening(object source) {
        // Подпишитесь на событие
        // Например, ((T)source).Event += EventHandler;
    }
    protected override void StopListening(object source) {
        // Отпишитесь от события
        // Например, ((T)source).Event -= EventHandler;
    }
    // Обработчик события – измените тип аргумента
    // на корректный для вашего события
    void EventHandler(object sender, EventArgs e) {
        base.DeliverEvent(sender, e);
    }
}


А это сниппет с шаблоном для Visual Studio: https://gist.github.com/777559. Вешается на команду «wem».

Этот шаблон можно использовать для любого диспетчера слабых событий. Вы должны изменить имя класса источника, способ подписки и скорректировать тип параметров в методе EventHandler.

Каждый объект, желающий подписаться на слабое событие, должен реализовать интерфейс IWeakEventListener. Этот интерфейс содержит единственный метод ReceiveWeakEvent. Вы должны проверить тип диспетчера событий, вызвать обработчик и вернуть true. Если вы не можете определить тип диспетчера, вы должны вернуть false. В этом случае будет вызвано исключение System.ExecutionEngineException с несколько непонятным текстом причины ошибки. По нему становится ясно, что в диспетчерах или прослушивателях есть ошибка.

Вот шаблон реализации интерфейса IWeakEventListener:

class MyEventListener : IWeakEventListener {
    bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) {
        if(managerType == typeof(MyEventManager)) {
            // Обработайте событие
            return true;    // Уведомление, что событие корректно обработано
        }
        return false;   // Что-то пошло не так
    }
}


Плюсы и минусы

  • Этот тип слабых событий вызывается практически мгновенно
  • Вы можете определить есть ли прослушиватели и не вызывать событие, если их нет. Для меня это было полезно в событиях, которые нужно вызывать очень часто или для событий с тяжелыми аргументами.
  • Могут использоваться для не UIElements. Полезно, если вы хотите использовать старый код из WPF.
  • Очень громоздки в создании – каждое событие требует своего диспетчера.


Routed Events (Перенаправленные события)


Перенаправленные события – это инфраструктура для событий, обрабатываемых в XAML (например, в EventTrigger) или проходящих по визуальному дереву.

Их главные преимущества:

  • Это слабые события.
  • Они могут вызывать обработчики на нескольких уровнях логического дерева.


В MSDN есть хорошая статья про них: Routed Events Overview и поэтому я не хочу повторять их здесь. Но упомяну два их основных недостатка.

Тяжелый вызов и нет информации о количестве подписчиков

Это часть метода UIElement.RaiseEventImpl, вызывающего перенаправленное событие:

internal static void RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
{
    EventRoute route = EventRouteFactory.FetchObject(args.RoutedEvent);
    if (TraceRoutedEvent.IsEnabled)
    {
        TraceRoutedEvent.Trace(TraceEventType.Start, TraceRoutedEvent.RaiseEvent, new object[] { args.RoutedEvent, sender, args, args.Handled });
    }
    try
    {
        args.Source = sender;
        BuildRouteHelper(sender, route, args);
        route.InvokeHandlers(sender, args);
        args.Source = args.OriginalSource;
    }
    finally
    {
        if (TraceRoutedEvent.IsEnabled)
        {
            TraceRoutedEvent.Trace(TraceEventType.Stop, TraceRoutedEvent.RaiseEvent, new object[] { args.RoutedEvent, sender, args, args.Handled });
        }
    }
    EventRouteFactory.RecycleObject(route);
}


Выглядит нормально, пока не взглянуть внутрь методов BuildRouteHelper и InvokeHandlers, каждый из которых длиннее 100 строк. И вся эта сложность для вызова единственного события. Такая сложность делает этот подход неприменимым для часто вызываемых событий.

Они могут быть добавлены только в наследников класса UIElement.

Вы не сможете создать перенаправленное событие для простого дата объекта, не наследуемого от UIElement. Если ваши технические требования не позволяют этого, то это станет проблемой для вас.

В итоге


Если вы не ограничены старым кодом и вызовы событий у вас не многочислены, то используйте RoutedEvents. Если вызовов много или у вас есть общий с .NET 2.0 код, то придется писать WeakEventManager для каждого. Громоздко, но придется.

Оба эти способа будут работать в MediumTrust. Если это требование для вас не важно, то ждите решения №3 в следующей серии.
Tags:
Hubs:
Total votes 55: ↑43 and ↓12 +31
Views 16K
Comments Comments 35

Information

Founded
1998
Location
Россия
Website
www.developersoft.ru
Employees
201–500 employees
Registered