Предотвращение утечек памяти с помощью прикрепленных поведений

Я создал «прикрепленное поведение» в своем приложении WPF, которое позволяет мне обрабатывать нажатие клавиши Enter и переходить к следующему элементу управления. Я называю это EnterKeyTraversal.IsEnabled, и вы можете увидеть код в моем блоге здесь .

Сейчас меня больше всего беспокоит то, что у меня может быть утечка памяти, поскольку я обрабатываю событие PreviewKeyDown в UIElements и никогда явно не «отсоединяю» событие.

Как лучше всего предотвратить эту утечку (если она действительно есть)? Следует ли мне вести список элементов, которыми я управляю, и отключать событие PreviewKeyDown в событии Application.Exit? Кто-нибудь добился успеха с прикрепленным поведением в своих собственных приложениях WPF и придумал элегантное решение для управления памятью?

Ответов (11)

Решение

Я не согласен DannySmurf

Некоторые объекты макета WPF могут засорять вашу память и сильно замедлять работу приложения, если они не собираются сборщиком мусора. Поэтому я считаю, что выбор слов правильный, вы утекаете память объектам, которые больше не используете. Вы ожидаете, что элементы будут собраны мусором, но это не так, потому что где-то есть ссылка (в данном случае в обработчике событий).

Теперь реальный ответ :)

Я советую вам прочитать эту статью о производительности WPF на MSDN.

Отказ от удаления обработчиков событий на объектах может сохранить объекты в живых

Делегат, который объект передает своему событию, фактически является ссылкой на этот объект. Следовательно, обработчики событий могут поддерживать объекты в живых дольше, чем ожидалось. При выполнении очистки объекта, который зарегистрировался для прослушивания события объекта, важно удалить этот делегат перед освобождением объекта. Сохранение живых ненужных объектов увеличивает использование памяти приложением. Это особенно верно, когда объект является корнем логического или визуального дерева.

Они советуют вам изучить паттерн Слабое событие.

Другое решение - удалить обработчики событий, когда вы закончите работу с объектом. Но я знаю, что с Attached Properties этот момент может быть не всегда ясен ..

Надеюсь это поможет!

Хорошо, что (менеджер немного) я, конечно, могу понять и посочувствовать.

Но как бы Microsoft ни называла это, я не думаю, что «новое» определение уместно. Это сложно, потому что мы не живем в полностью управляемом мире (хотя Microsoft любит делать вид, что мы живем, сама Microsoft не живет в таком мире). Когда вы говорите об утечке памяти, вы можете иметь в виду, что программа потребляет слишком много памяти (это определение пользователя), или что управляемая ссылка не будет освобождена до выхода (как здесь), или что неуправляемая ссылка не очищается должным образом. вверх (это была бы настоящая утечка памяти), или этот неуправляемый код, вызванный из управляемого кода, вызывает утечку памяти (еще одна реальная утечка).

В этом случае очевидно, что означает «утечка памяти», хотя мы и неточны. Но становится ужасно утомительно разговаривать с некоторыми людьми, которые называют каждое чрезмерное потребление или неспособность собрать утечку памяти; и неприятно, когда эти люди программисты, которые якобы знают лучше. Я думаю, что для технических терминов важно иметь однозначное значение. Когда они это делают, отладка становится намного проще.

В любом случае. Не хочу превращать это в легкую сказочную дискуссию о языке. Просто говорю...

Чтобы объяснить мой комментарий к сообщению Джона Фентона, вот мой ответ. Давайте посмотрим на следующий пример:

class Program
{
    static void Main(string[] args)
    {
        var a = new A();
        var b = new B();

        a.Clicked += b.HandleClicked;
        //a.Clicked += B.StaticHandleClicked;
        //A.StaticClicked += b.HandleClicked;

        var weakA = new WeakReference(a);
        var weakB = new WeakReference(b);

        a = null;
        //b = null;

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

        Console.WriteLine("a is alive: " + weakA.IsAlive);
        Console.WriteLine("b is alive: " + weakB.IsAlive);
        Console.ReadKey();
    }


}

class A
{
    public event EventHandler Clicked;
    public static event EventHandler StaticClicked;
}

class B
{
    public void HandleClicked(object sender, EventArgs e)
    {
    }

    public static void StaticHandleClicked(object sender, EventArgs e)
    {
    }
}

Если у вас есть

a.Clicked += b.HandleClicked;

и установите только b в ноль, обе ссылки weakA и weakB остаются в живых! Если вы установите только a в значение null, b остается в живых, но не a (что доказывает, что Джон Фентон ошибается, заявляя, что жесткая ссылка хранится в поставщике событий - в данном случае a).

Это привело меня к НЕПРАВИЛЬНОМУ выводу, что

a.Clicked += B.StaticHandleClicked;

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

A.StaticClicked += b.HandleClicked;

ссылка будет сохранена на b.

Я только что прочитал твое сообщение в блоге и думаю, ты получил вводящий в заблуждение совет, Мэтт. Если здесь действительно есть утечка памяти , то это ошибка .NET Framework, а не то, что вы обязательно можете исправить в своем коде.

Я думаю, что вы (и постер в вашем блоге) на самом деле говорите здесь не об утечке, а, скорее, о продолжающемся потреблении памяти. Это не одно и то же. Чтобы было ясно, утечка памяти - это память, которая зарезервирована программой, а затем заброшена (т. Е. Указатель остается висящим) и которую впоследствии нельзя освободить. Поскольку память управляется в .NET, это теоретически невозможно. Однако программа может зарезервировать постоянно увеличивающийся объем памяти, не позволяя ссылкам на нее выходить за пределы области видимости (и становиться доступной для сборки мусора); однако эта память не просочилась. ГХ вернет его в систему после выхода из вашей программы.

Так. Отвечая на ваш вопрос, я не думаю, что у вас здесь действительно проблема. У вас определенно нет утечки памяти, и, судя по вашему коду, я не думаю, что вам нужно беспокоиться о потреблении памяти. До тех пор, пока вы убедитесь, что вы не назначаете этот обработчик событий повторно, не отменяя его (то есть, вы либо устанавливаете его только один раз, либо удаляете его ровно один раз при каждом назначении), что вы, кажется, делаете, ваш код должен быть в порядке.

Похоже, это совет, который пытался дать вам постер в вашем блоге, но он использовал ту тревожную «утечку» работы, что является пугающим словом, но многие программисты забыли реальное значение этого слова в управляемом мире; здесь это не применяется.

Убедитесь, что элементы, ссылающиеся на события, находятся в объекте, на который они ссылаются, например текстовые поля в элементе управления формы. Или если этого нельзя предотвратить. Создайте статическое событие в глобальном вспомогательном классе, а затем отслеживайте события в глобальном вспомогательном классе. Если эти два шага не могут быть выполнены, попробуйте использовать WeakReference, они обычно идеально подходят для таких ситуаций, но сопряжены с накладными расходами.

@Nick Да, дело в том, что прикрепленные поведения по определению находятся не в том же объекте, что и элементы, события которых вы обрабатываете.

Я думаю, что ответ заключается в том, чтобы каким-то образом использовать WeakReference, но я не видел простых примеров кода, чтобы объяснить это мне. :)

Вы когда-нибудь задумывались о реализации «паттерна слабых событий» вместо обычных событий?

  1. Шаблон слабого события в WPF
  2. Шаблоны слабых событий (MSDN)

@Arcturus:

... засоряют вашу память и замедляют работу вашего приложения, когда они не собираются сборщиком мусора.

Это совершенно очевидно, и я не возражаю. Тем не мение:

... у вас утечка памяти для объектов, которые вы больше не используете ... потому что на них есть ссылка.

«память выделяется программе, и эта программа впоследствии теряет возможность доступа к ней из-за ошибок в программной логике» (Википедия, «Утечка памяти»)

Если есть активная ссылка на объект, к которому ваша программа может получить доступ, то по определению это не утечка памяти. Утечка означает, что объект больше не доступен (для вас или для OS / Framework) и не будет освобожден на время существования текущего сеанса операционной системы . Здесь дело обстоит не так.

(Извините за семантический нацист ... возможно, я немного придерживаюсь старой школы, но утечка имеет очень специфическое значение. В наши дни люди склонны использовать термин «утечка памяти» для обозначения всего, что потребляет на 2 КБ памяти больше, чем они хотят. ..)

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

Да, я знаю, что в старые времена утечки памяти - это совсем другая тема. Но с управляемым кодом новое значение термина утечка памяти может быть более подходящим ...

Microsoft даже признает, что это утечка памяти:

Зачем применять шаблон WeakEvent?

Прослушивание событий может привести к утечке памяти. Типичным методом прослушивания события является использование синтаксиса, зависящего от языка, который прикрепляет обработчик к событию в источнике. Например, в C# это синтаксис: source.SomeEvent + = new SomeEventHandler (MyEventHandler).

Этот метод создает надежную ссылку от источника события на прослушиватель событий. Обычно присоединение обработчика событий для слушателя приводит к тому, что у слушателя есть время жизни объекта, на которое влияет время жизни объекта для источника (если только обработчик событий не удален явно). Но в определенных обстоятельствах вы можете захотеть, чтобы время жизни объекта слушателя контролировалось только другими факторами, такими как то, принадлежит ли он в настоящее время визуальному дереву приложения, а не временем жизни источника. Всякий раз, когда время жизни исходного объекта превышает время жизни объекта слушателя, обычный шаблон событий приводит к утечке памяти: слушатель остается в живых дольше, чем предполагалось.

Мы используем WPF для клиентского приложения с большими ToolWindows, которые можно перетаскивать, все изящные вещи, и все это совместимо с XBAP .. Но у нас была та же проблема с некоторыми ToolWindows, которые не были собраны мусором .. Это было связано с к тому факту, что он все еще зависел от прослушивателей событий ... Теперь это может не быть проблемой, когда вы закрываете окно и завершаете работу приложения. Но если вы создаете очень большие ToolWindows с большим количеством команд, и все эти команды пересматриваются снова и снова, и люди должны использовать ваше приложение весь день ... Я могу вам сказать ... это действительно забивает вашу память и время отклика вашего приложения ..

Кроме того, мне гораздо проще объяснить моему руководителю, что у нас есть утечка памяти, чем объяснять ему, что некоторые объекты не собираются мусором из-за некоторых событий, которые требуют очистки;)

Правда правда,

Вы, конечно, правы ... Но в этом мире рождается целое поколение программистов, которые никогда не коснутся неуправляемого кода, и я верю, что определения языков будут изобретать себя снова и снова. Таким образом, утечки памяти в WPF отличаются от C / Cpp.

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

Что касается проблемы Мэтта, это может быть проблема с производительностью, которую вам, возможно, придется решить. Если вы просто используете несколько экранов и сделаете эти элементы управления экраном одиночными, вы можете вообще не увидеть этой проблемы;).

Помимо философских дебатов, глядя на сообщение в блоге OP, я не вижу здесь утечек:

ue.PreviewKeyDown += ue_PreviewKeyDown;

Жесткая ссылка на ue_PreviewKeyDown хранится в ue.PreviewKeyDown .

ue_PreviewKeyDown это STATIC метод и не может быть GCed .

Никаких жестких ссылок на ue не сохраняется, поэтому ничто не препятствует их созданию GCed .

Итак ... Где утечка?