В этой статьи озвучена тема утечек памяти при программировании на WPF. Информация для материала взята
с источников, приведенных в списке литературы. Это было сделано для того, чтобы
информация об утечках памяти при проектировании и программировании на WPF оставалась
актуальной, так как ссылки в интернете часто теряются. Пожалуй, начнем из самых
популярных утечек, которые разработчики часто совершают при разработке программного
обеспечения (ПО).
Неотписанные
события (Unregistered Events)
Опишем гипотетический пример,
в котором проявляется утечка памяти, если
не отписаться от события в родительском элементе.
public void AddBook()
{
var book = new LibraryBook
{
Author = "Jon Skeet",
Title = "C# in Depth",
Count = 3,
SN = "ISBN: 9781617291340",
Year = new DateTime(2013, 9, 10)
};
_books.Add(book);
book.RemoveBook += OnRemoveBook;
}
public void OnRemoveBook(LibraryBook book)
{
_books.Remove(book);
}
В данном примере ошибка
заключается в том, что событие RemoveBook
содержит
ссылку на объект LibraryBook,
и даже после того, как мы удалили объект из коллекции, он все еще продолжает
жить. Вот как будет выглядеть исправление данной утечки:
public void OnRemoveBook(LibraryBook book)
{
_books.Remove(book);
book.RemoveBook -= OnRemoveBook;
}
После этого GC подберёт
весь мусор и очистит память, которую занимал данный объект.
Данная утечка
памяти имеет место при разработке программ на управляемых языках. Эта проблема
относится не только к WPF
и
Silverlight,
а также к программам, написанным на VB.NET, Windows
Forms,
Console
Application,
а
также имеет место везде, где используется модель событий .Net.
Databinding
Утечка памяти может произойти,
если имеем дочерний объект, который связывается через байндинг со свойством
родителя.
<TextBlock Name="txtMainText" Text="{Binding ElementName=mainGrid, Path=Children.Count}" />
</Grid>
Условие будет выполняться только тогда, когда связанное свойство является свойством PropertyDescriptor,
каким и является Children.Count. Это потому, что с целью определения изменений свойств, происходящих в PropertyDescriptor, фреймфорк должен
подписаться на событие ValueChanged, что, в свою очередь, создаёт сильную цепную
зависимость. Эту проблему можно решить несколькими способами:
- Добавить необходимое DependencyProperty, которое будет просто возвращать значение, необходимое свойству PropertyDescriptor.
- Установить свойство Mode для байндинга в OneTime.
- Предоставить интерфейс INotifyProipertyChanged для объекта (ссылка).
- Добавить очистку байндинга после выхода со страници (OnClose).
BindingOperations.ClearBinding(txtMainText, TextBlock.TextProperty);
Статические
события (Static
event)
Подписка на событие в
статическом классе создает сильное связывание на любые объекты, которые
обрабатывают это событие. Использование подписок на статические события часто может приводить к утечке ресурсов. Строгие ссылки препятствуют сборке
мусора, и это является основной причиной того, что Ваши объекты не будут уничтожены. Чтобы уничтожить ссылку на
статический класс, достаточно просто отписаться от события, например, при закрытии
окна и т.д. Одним из способов борьбы с утечкой памяти при использовании
статических событий является использование шаблона слабых событий (weak event patterns). Об этом можно прочитать в моей статье Слабые события в C#.
Command Binding
Command binding -
очень полезная возможность, которую нам предоставляет WPF. В данном примере речь пойдет о
классе CommonBinding,
который позволяет использовать в Ваших приложениях общие команды, доступные для
всего приложения (common
application
command)
такие как Cut,
Copy,
Undo
и
т.д. Данный подход приведет к утечке памяти, если в родительском классе будет
содержаться ссылка на дочерний объект.
CommandBinding cutCmdBinding = new CommandBinding(ApplicationCommands.Cut, OnMyCutHandler,
OnCanICut);
mainWindow.main.CommandBindings.Add(cutCmdBinding);
…..
void OnMyCutHandler (object target, ExecutedRoutedEventArgs e)
{
MessageBox.Show("You attempted to CUT");
}
void OnCanICut (object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
В данном случае утечка
происходит в коде mainWindow.main.CommandBindings; мы имеем строгую ссылку на дочерний объект. В
результате события, когда дочерний объект (child) закрывается, объект все еще будет
оставаться в памяти в связи с тем, что на него ссылается родительский объект. Решение
этой проблемы очень простое: после закрытия дочернего окна (элемента) нужно удалить
ссылку на него ссылку в родителя.
mainWindow.main.CommandBindings.Remove(cutCmdBinding);
DispatcherTimer
Неправильное использование
объекта DispatcherTimer
также может вызвать утечку памяти. Приведенный ниже код создает новый DispatcherTimer в элементе управления
пользователя (UserControl),
и, чтобы легче увидеть утечки, в код также добавлен массив байт, который
называется myMemory.
public byte[] myMemory = new byte[50 * 1024 * 1024];
System.Windows.Threading.DispatcherTimer _timer = new System.Windows.Threading.DispatcherTimer();
int count = 0;
private void MyLabel_Loaded(object sender, RoutedEventArgs e)
{
_timer.Interval = TimeSpan.FromMilliseconds(1000);
_timer.Tick += new EventHandler(delegate(object s, EventArgs ev)
{
count++;
textBox1.Text =
count.ToString();
});
_timer.Start();
}
В данном примере утечки
памяти можно добиться таким способом: добавить дочерние UserControl, которые
используют объект класса DispatcherTimer, например, в StackPanel.
Затем с помощью кнопки запрограммировать удаление данных контролов со StackPanel. Когда Вы запустите, например, ANTS профайлер,
то сможете увидеть утечку. Проблема данного подхода в том, что класс Dispatcher,
который
используется для предоставления доступа к главному потоку формы, удерживает объекты DispatcherTimers, которые не
уничтожены. Чтобы избежать данной утечки памяти, необходимо просто
остановить таймер и обнулить его.
_timer.Stop();
_timer =
null;
Использование BitmapImage в Image Source
bi1 = //bi1 is
static
new BitmapImage(new Uri("Bitmap1.bmp", UriKind.RelativeOrAbsolute));
//bi1.Freeze() //if you do not
Freeze, your app will leak memory
m_Image1 = new Image();
m_Image1.Source = bi1;
MyStackPanel.Children.Add(m_Image1);
Эта утечка срабатывает, потому что
WPF держит
строгую ссылку между статической переменной BitmapImage (bi1) и Image (m_Image1). BitmapImage (bi1) объявляется как статическая
переменная, поэтому она не подбирается сборщиком мусора, когда мы закрываем
окно, которое использует данный код. Эта утечка может произойти только при
использовании BitmapImage.
Она не появляется при использовании, например, DrawingImage. Обходной путь зависит
от используемого сценария. Один из способов - заморозить
BitmapImage через метод Freeze.
WPF не перехватывает события для объектов, которые заморожены. В общем, нужно
заморозить объекты, когда это возможно, чтобы улучшить производительность
приложения. Дополнительно можно посмотреть здесь. С
использованием BitmapImage
связано
еще несколько нюансов, с которыми можно ознакомиться по данной ссылке: WPF Performance and .NET Framework Client Profile.
Итоги
В данной
статье рассмотрены популярные утечки памяти при программировании на WPF, а также способы их решения.
Надеюсь, статья получилась не очень скучной и поможет Вам избежать утечек
памяти в дальнейшем.
Источники:
No comments:
Post a Comment