В этой статье я
бы хотел поговорить с вами об утечках памяти в WPF.
Эта тема уже затрагивалась в статье "Утечки памяти в
WPF". Сегодня мы разберем только одну из утечек, связанную с
байндингом на List<T>. Вы,
наверное, знаете, почему не стоить делать байндинг на список List<T>, если не
использовать интерфейс INotifyPropertyChanged или INotifyCollectionChanged. К чему это может привести? Если
вам интересно, что такого происходит в .NET
Framework, что может привести к утечке памяти, то
мы начнем наше плавное погружение в исходный код .NET Framework,
чтобы понять принцип работы байндинга в WPF
и
почему не стоит связываться со списками List<T>. Мне понравилось описание со stackoverflow
с кратким объяснением причин утечки памяти в WPF
при
подписке на List<T>.
If you are not binding to a DependencyProperty or a object that
implements INotifyPropertyChanged then the binding can leak memory, and you
will have to unbind when you are done.
This is because if the object is not a DependencyProperty or does not
implement INotifyPropertyChanged then it uses the ValueChanged event via the PropertyDescriptors
AddValueChanged method. This causes the CLR to create a strong reference from
the PropertyDescriptor to the object and in most cases the CLR will keep a
reference to the PropertyDescriptor in a global table.
Because the binding must continue to listen for changes. This behavior
keeps the reference alive between the PropertyDescriptor and the object as the
target remains in use. This can cause a memory leak in the object and any object
to which the object refers, This includes the data-binding target.
So in short if you are binding to a DependencyProperty or
InotifyPropertyChanged object then you sould be ok, othewise like any
subscribed event you should unsubscribe your bindings.
Если вкратце, то
речь идет о том, что если вы биндитесь не на DependencyProperty и ваш объект не реализует интерфейс INotifyPropertyChanged, то может произойти утечка
памяти. Причиной этого может быть то, что объект, который не реализует INotifyPropertyChaned и не является DependencyProperty, использует событие ValueChanged через метод класса PropertyDescriptor AddValueChanged. Это приведет к тому, что CLR будет держать строгую ссылку на PropertyDescriptor в глобальной
таблице. Поскольку объект продолжает прослушиваться, такое поведение сохраняет
ссылку между PropertyDescriptor и целевым объектом подписки. Это
может привести к утечке памяти в самом объекте, а также в любых объектах, которые
относятся к данному объекту.
Давайте немного
разберемся с тем, как это работает. Для начала у нас есть несколько ключевых
классов, на которых ложится вся логика, связанная с байндингом. Первым классом в
данном списке, который вы используете явно, является класс Binding,
наследуемый от класса BindableBase. В этом классе
доступные все свойства, которые мы задаем в основном через XAML-разметку или в редких случаях через код. Ниже на
картинке приведен список доступных свойств класса Binding.
Примечание: В .NET Framework 4.5 компания Microsoft исправила множество мест с байндингом, которые
содержали жесткую ссылку на элемент, с использованием паттерна шаблона слабых событий,
а также класса WeakReference для
использования мягких ссылок. Ниже приведен пример установки свойства Source в байдинге.
/// <summary>
object to use as the source </summary>
/// <remarks>
To clear this property, set it to DependencyProperty.UnsetValue. </remarks>
public object
Source
{
get
{
WeakReference<object>
wr = (WeakReference<object>)GetValue(Feature.ObjectSource,
null);
if (wr == null)
return null;
else
{
object target;
return wr.TryGetTarget(out
target) ? target : null;
}
}
set
{
CheckSealed();
if (_sourceInUse == SourceProperties.None
|| _sourceInUse == SourceProperties.Source)
{
if (value != DependencyProperty.UnsetValue)
{
SetValue(Feature.ObjectSource,
new WeakReference<object>(value));
SourceReference = new ExplicitObjectRef(value);
}
else
{
ClearValue(Feature.ObjectSource);
SourceReference = null;
}
}
else
throw new InvalidOperationException(SR.Get(SRID.BindingConflict, SourceProperties.Source, _sourceInUse));
}
}
Это лишь малая
часть айсберга. Для того чтобы устанавливать значения для свойств класса Binding, используется класс BindingExpression.
Надеюсь, вы не забыли, что класс Binding наследуется от
класса BindableBase, в этом классе происходит создание BindingExpression и привязка к указанному объекту,
а также его свойствам.
/// <summary>
///
Return the value to set on the property for the target for this
///
binding.
/// </summary>
public sealed override object
ProvideValue(IServiceProvider
serviceProvider)
{
// Binding a property value only works on DependencyObject and
DependencyProperties.
// For all other cases, just return this Binding object as the value.
if (serviceProvider == null)
{
return this;
}
// Bindings are not allowed On CLR props except for
Setter,Trigger,Condition (bugs 1183373,1572537)
DependencyObject targetDependencyObject;
DependencyProperty targetDependencyProperty;
Helper.CheckCanReceiveMarkupExtension(this,
serviceProvider, out targetDependencyObject, out
targetDependencyProperty);
if (targetDependencyObject == null
|| targetDependencyProperty == null)
{
return this;
}
// delegate real work to subclass
return
CreateBindingExpression(targetDependencyObject, targetDependencyProperty);
}
Подпиской на
события об обновлении свойств оперирует класс BindingWorker.
Этот класс абстрактный и имеет две реализации: XmlBindingWorker
и ClrBindingWorker.
XmlBindingWorker, хоть и
наследуется от класса BindingWorker, но является своего рода декоратором для класса ClrBindingWorker и используется для работы с XML. Класс BindingExpression, который мы рассмотрели вскользь выше,
использует в себе класс BindingWorker (по сути, используется класс ClrBindingWorker) для подписки
на изменения элементов.
private void
CreateWorker()
{
Invariant.Assert(Worker == null, "duplicate
call to CreateWorker");
_worker = new ClrBindingWorker(this, Engine);
}
Теперь, наконец,
мы подошли к классу ClrBindingWorker. Этот класс является
своего рода менеджером, так как он управляет установкой связывания.
Непосредственно самим связыванием управляет класс PropertyPathWorker,
а классом ParopertyPathWorker управляет уже
класса ClrBindingWorker.
internal ClrBindingWorker(BindingExpression
b, DataBindEngine engine) : base(b)
{
PropertyPath
path = ParentBinding.Path;
if (ParentBinding.XPath != null)
{
path = PrepareXmlBinding(path);
}
if
(path == null)
{
path = new PropertyPath(String.Empty);
}
if (ParentBinding.Path == null)
{
ParentBinding.UsePath(path);
}
_pathWorker = new PropertyPathWorker(path,
this, IsDynamic, engine);
_pathWorker.SetTreeContext(ParentBindingExpression.TargetElementReference);
}
Чтобы не
запутаться, рекомендую посмотреть на код выше, чтобы понять, кто от кого
зависит и кто кем оперирует. Теперь более детально ознакомимся с классом PropertyPathWorker.
Этот класс и занимается уведомлением об изменении значений, обновлении целевого
объекта и т.д. А как он это делает, мы сейчас разберем детально, так как мы, по
сути, добрались до основного класса, который управляет обновлением привязки.
Если вы перейдете по ссылке для класса PropertyPathWorker,
приведенную в предыдущем предложении, то сможете найти функцию UpdateSourceValueState.
// fill in the SourceValueState with updated
infomation, starting at level k+1.
// If view isn't null, also update the
current item at level k.
private void
UpdateSourceValueState(int k, ICollectionView
collectionView)
{
UpdateSourceValueState(k, collectionView, BindingExpression.NullDataItem,
false);
}
// fill in the SourceValueState with updated
infomation, starting at level k+1.
// If view isn't null, also update the
current item at level k.
private void
UpdateSourceValueState(int k, ICollectionView
collectionView, object newValue, bool
isASubPropertyChange)
{
//
give host a chance to shut down the binding if the target has
//
gone away
DependencyObject
target = null;
if
(_host != null)
{
target = _host.CheckTarget();
if (_rootItem != BindingExpression.NullDataItem
&& target == null)
return;
}
int
initialLevel = k;
object
rawValue = null;
//
optimistically assume the new value will fix previous path errors
_status = PropertyPathStatus.Active;
//
prepare to collect changes to dependency sources
_dependencySourcesChanged = false;
//
Update the current item at level k, if requested
if
(collectionView != null)
{
Debug.Assert(0<=k &&
k<_arySVS.Length && _arySVS[k].collectionView == collectionView, "bad
parameters to UpdateSourceValueState");
ReplaceItem(k, collectionView.CurrentItem, NoParent);
}
//
update the remaining levels
for
(++k; k<_arySVS.Length; ++k)
{
isASubPropertyChange = false; // sub-property changes
only matter at the last level
ICollectionView oldCollectionView =
_arySVS[k].collectionView;
// replace the item at level k using parent from level k-1
rawValue = (newValue == BindingExpression.NullDataItem)
? RawValue(k-1) : newValue;
newValue = BindingExpression.NullDataItem;
if (rawValue == AsyncRequestPending)
{
_status = PropertyPathStatus.AsyncRequestPending;
break; //
we'll resume the loop after the request completes
}
ReplaceItem(k, BindingExpression.NullDataItem,
rawValue);
// replace view, if necessary
ICollectionView newCollectionView =
_arySVS[k].collectionView;
if (oldCollectionView != newCollectionView && _host != null)
{
_host.ReplaceCurrentItem(oldCollectionView, newCollectionView);
}
}
//
notify binding about what happened
if
(_host != null)
{
if (initialLevel < _arySVS.Length)
{
// when something in the path changes, recompute whether we
// need direct notifications from the raw value
NeedsDirectNotification = _status == PropertyPathStatus.Active
&&
_arySVS.Length > 0
&&
SVI[_arySVS.Length-1].type
!= SourceValueType.Direct &&
!(_arySVS[_arySVS.Length-1].info is DependencyProperty)
&&
typeof(DependencyObject).IsAssignableFrom(_arySVS[_arySVS.Length-1].type);
}
_host.NewValueAvailable(_dependencySourcesChanged, initialLevel < 0,
isASubPropertyChange);
}
GC.KeepAlive(target); // keep target alive
during changes (bug 956831)
}
Эта функция
заполняет SourceValueState, а также
позволяет заменить данные с полученного айтема. Как это работает, можно увидеть ниже.
internal void
OnPropertyChangedAtLevel(int level)
{
UpdateSourceValueState(level, null);
}
internal void
OnCurrentChanged(ICollectionView
collectionView)
{
for (int
k=0; k<Length; ++k)
{
if (_arySVS[k].collectionView == collectionView)
{
_host.CancelPendingTasks();
// update everything below that level
UpdateSourceValueState(k, collectionView);
break;
}
}
}
internal void
OnDependencyPropertyChanged(DependencyObject
d, DependencyProperty dp, bool
isASubPropertyChange)
{
if
(dp == DependencyObject.DirectDependencyProperty)
{
// the only way we get notified about this property is when the raw
// value reports a subProperty change.
UpdateSourceValueState(_arySVS.Length, null, BindingExpression.NullDataItem,
isASubPropertyChange);
return;
}
//
find the source level where the change happened
int
k;
for
(k=0; k<_arySVS.Length; ++k)
{
if ((_arySVS[k].info == dp) && (BindingExpression.GetReference(_arySVS[k].item) == d))
{
// update everything below that level
UpdateSourceValueState(k, null, BindingExpression.NullDataItem,
isASubPropertyChange);
break;
}
}
}
internal void
OnNewValue(int level, object value)
{
//
optimistically assume the new value will fix previous path errors
_status = PropertyPathStatus.Active;
if
(level < Length - 1)
UpdateSourceValueState(level, null,
value, false);
}
Когда мы
добавляем новое значение или получаем уведомление, что у нас что-то
изменилось, мы сразу же вызываем функцию UpdateSourceValueState. В этой функции мы можем увидеть, как происходит подписка и
отписка на изменения нового айтема (прослушка элемента), посмотрев метод ReplaceItem. Понять принцип работы метода ReplaceItem несложно; хотя он большой, но для понимания достаточно просто вдумчиво взглянуть в написанный код. Но
так как нас больше интересует, как происходит уведомление, мы рассмотрим только
как происходит отписка на айтем, а затем посмотрим, как работает подписка.
Первым делом начнем рассмотрение с отписки от айтема по переданной позиции.
// replace the item at level k with the given
item, or with an item obtained from the given parent
private void
ReplaceItem(int k, object
newO, object parent)
{
bool
isExtendedTraceEnabled = IsExtendedTraceEnabled(TraceDataLevel.ReplaceItem);
SourceValueState
svs = new SourceValueState();
object
oldO = BindingExpression.GetReference(_arySVS[k].item);
//
stop listening to old item
if
(IsDynamic && SVI[k].type != SourceValueType.Direct)
{
INotifyPropertyChanged oldPC;
DependencyProperty oldDP;
PropertyInfo oldPI;
PropertyDescriptor oldPD;
DynamicObjectAccessor oldDOA;
PropertyPath.DowncastAccessor(_arySVS[k].info, out
oldDP, out oldPI, out oldPD, out
oldDOA);
if (newO == BindingExpression.StaticSource)
{
Type declaringType = (oldPI != null)
? oldPI.DeclaringType
: (oldPD != null)
? oldPD.ComponentType
: null;
if (declaringType != null)
{
StaticPropertyChangedEventManager.RemoveHandler(declaringType,
OnStaticPropertyChanged, SVI[k].propertyName);
}
}
else if (oldDP != null)
{
_dependencySourcesChanged = true;
}
else if ((oldPC = oldO as INotifyPropertyChanged)
!= null)
{
PropertyChangedEventManager.RemoveHandler(oldPC,
OnPropertyChanged, SVI[k].propertyName);
}
else if (oldPD != null && oldO != null)
{
ValueChangedEventManager.RemoveHandler(oldO,
OnValueChanged, oldPD);
}
}
// Упущенный код который нам не нуже для рассмотрения
}
В данном методе
мы создаем несколько переменных, для того чтобы определить, с каким айтемом мы
работаем и на какое изменение событий нам подписаться.
- INotifyPropertyChanged – интерфейс, который мы используем в WPF для уведомления об изменении какого-то значения;
- DependencyProperty представляет собой свойство, которое может быть установлено такими способами, как стилизация, привязка данных, анимация и наследование;
- PropertyInfo позволяет получить доступ к свойства с помощью атрибутов;
- PropertyDescriptor – класс, который позволяет уведомлять другие объекты об изменении какого-то свойства;
- DynamicObjectAccessor – внутренний класс, который используется для работы с динамическими объектами.
Метод DowncastAccessor просто
проставляет данные свойства.
// Convert an "accessor" into one
of the legal types
internal static void
DowncastAccessor(object accessor,
out DependencyProperty
dp, out PropertyInfo pi, out PropertyDescriptor
pd, out DynamicObjectAccessor doa)
{
if
((dp = accessor as DependencyProperty)
!= null)
{
pd = null;
pi = null;
doa = null;
}
else if
((pi = accessor as PropertyInfo)
!= null)
{
pd = null;
doa = null;
}
else if
((pd = accessor as PropertyDescriptor)
!= null)
{
doa = null;
}
else
{
doa = accessor as DynamicObjectAccessor;
}
}
Затем мы смотрим, какое
значение к нам пришло.
if (newO == BindingExpression.StaticSource)
{
Type
declaringType = (oldPI != null) ? oldPI.DeclaringType
: (oldPD != null)
? oldPD.ComponentType
: null;
if
(declaringType != null)
{
StaticPropertyChangedEventManager.RemoveHandler(declaringType,
OnStaticPropertyChanged, SVI[k].propertyName);
}
}
else if
(oldDP != null)
{
_dependencySourcesChanged = true;
}
else if
((oldPC = oldO as INotifyPropertyChanged)
!= null)
{
PropertyChangedEventManager.RemoveHandler(oldPC,
OnPropertyChanged, SVI[k].propertyName);
}
else if
(oldPD != null && oldO != null)
{
ValueChangedEventManager.RemoveHandler(oldO,
OnValueChanged, oldPD);
}
Если новый
объект равен StaticSource, то пытаемся удалить подписку на данный айтем с
помощью StaticPropertyChangedEventManager.
Если же айтем, который пришел к нам, не равен свойству StaticSource,
то смотрим по переменным, которые нам возвратил метод DowncastAccessor. Если тот айтем, который пришел к нам,
является DependencyPropert, то просто устанавливаем булевую
переменную _dependencySourcesChanged в значение true. Затем проверяем, не наследуется ли наш айтем от
интерфейса INotifyPropertyChanged, и если он наследуется и при
этом не равен null, то можно удалить подписчик на изменения данного
айтема с помощью PropertyChangedEventManager.
И последняя проверка: если полученный ранее объект не равен null и переменная oldDP класса PropertyDescriptor не равна null, то отписываемся от события с помощью
менеджера ValueChangedEventManager.
Все эти менеджеры реализуют паттерн, который называется "шаблон слабых
событий" и который представлен в языке C#
реализацией класса WeakEventManager.
В самом начале статьи есть ссылка на статью, в которой рассматривается
использование данного паттерна в языке C#
более детально.
Нам осталось
рассмотреть, как происходит подписка на отслеживание изменений в переданного
айтема.
if (IsDynamic &&
SVI[k].type != SourceValueType.Direct)
{
Engine.RegisterForCacheChanges(newO, svs.info);
INotifyPropertyChanged
newPC;
DependencyProperty
newDP;
PropertyInfo
newPI;
PropertyDescriptor
newPD;
DynamicObjectAccessor
newDOA;
PropertyPath.DowncastAccessor(svs.info,
out newDP, out newPI, out
newPD, out newDOA);
if
(newO == BindingExpression.StaticSource)
{
Type declaringType = (newPI != null)
? newPI.DeclaringType
: (newPD != null)
? newPD.ComponentType
: null;
if (declaringType != null)
{
StaticPropertyChangedEventManager.AddHandler(declaringType,
OnStaticPropertyChanged, SVI[k].propertyName);
}
}
else if
(newDP != null)
{
_dependencySourcesChanged = true;
}
else if
((newPC = newO as INotifyPropertyChanged)
!= null)
{
PropertyChangedEventManager.AddHandler(newPC,
OnPropertyChanged, SVI[k].propertyName);
}
else if
(newPD != null && newO != null)
{
ValueChangedEventManager.AddHandler(newO,
OnValueChanged, newPD);
}
}
Отличий от
отписки ,которую мы рассмотрели выше, практически нет. Разве что для того чтобы
отписаться от отслеживания изменений с помощью событий с приставкой RemoveHandler, мы используем добавление этих событий
с помощью методов AddHandler. Ниже на
рисунке представлена компактная схемка, демонстрирующая, что с чем связывается и какой класс за что
отвечает, так как Visual Studio 2013 не смог сгенерировать нормальную диаграмму классов.
Итоги
Если вы
используете .NET Framework 4.5, то у вас не будет утечки
памяти, как вы можете увидеть из рассмотренного примера. Microsoft сделала небольшие изменения в коде с
использованием слабых ссылок, поэтому ссылка не будет храниться долгое время в
памяти. Как только объект станет недоступен, слушатели, которые реализуют
менеджеры, приведенные на рисунке выше, почистят все за собой. Статья получилась
больше не о том, где и почему произойдет утечка памяти, а о том, как работает
вообще обновление объектов в WPF, для понимания, что может грозить в случае использования того или иного подхода. Надеюсь, что
статья получилась не слишком заумной и запутанной и, возможно, вам пригодится в
более глубоком понимании работы байндинга.
No comments:
Post a Comment