Skip to content

Commit 6f073d6

Browse files
[controls] fix memory leak with CarouselView & INotifyCollectionChanged (#18267)
Context: #17726 In investigating #17726, I found a memory leak with `CarouselView`: 1. Have a long-lived `INotifyCollectionChanged` like `ObservableCollection`, that lives the life of the application. 2. Set `CarouselView.ItemsSource` to the collection. 3. `CarouselView` will live forever, even if the page is popped, etc. I further expanded upon `MemoryTests` to assert more things for each control, and was able to reproduce this issue. To fully solve this, I had to fix issues on each platform. ~~ Android ~~ `ObservableItemsSource` subscribes to the `INotifyCollectionChanged` event, making it live forever in this case. To fix this, I used the same technique in 58a42e5: `WeakNotifyCollectionChangedProxy`. `ObservableItemsSource` is used for `ItemsView`-like controls, so this may fix other leaks as well. ~~ Windows ~~ Windows has the same issue as Android, but for the following types: * `CarouselViewHandler` subscribes to `INotifyCollectionChanged` * `ObservableItemTemplateCollection` subscribes to `INotifyCollectionChanged` These could be fixed with `WeakNotifyCollectionChangedProxy`. Then I found another issue with `ItemTemplateContext` These three types are used for other `ItemsView`-like controls, so this may fix other leaks as well. ~~ iOS/Catalyst ~~ These platforms suffered from multiple circular references: * `CarouselViewController` -> `CarouselView` -> `CarouselViewHandler` -> `CarouselViewController` * `ItemsViewController` -> `CarouselView` -> `CarouselViewHandler` -> `ItemsViewController` (`CarouselViewController` subclasses this) * `CarouselViewLayout` -> `CarouselView` -> `CarouselViewHandler` -> `CarouselViewLayout` The analyzer added in 2e65626 did not yet catch these because I only added the analyzer so far for `Microsoft.Maui.dll` and these issues were in `Microsoft.Maui.Controls.dll`. In a future PR, we can proactively try to add the analyzer to all projects. ~~ Conclusion ~~ Unfortunately, this does not fully solve #17726, as there is at least one further leak on Android from my testing. But this is a good step forward.
1 parent 25249e8 commit 6f073d6

File tree

10 files changed

+153
-72
lines changed

10 files changed

+153
-72
lines changed

src/Controls/src/Core/Handlers/Items/Android/ItemsSources/ItemsSourceFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static IItemsViewSource Create(IEnumerable itemsSource, BindableObject co
2020
case IList list when itemsSource is INotifyCollectionChanged:
2121
return new ObservableItemsSource(new MarshalingObservableCollection(list), container, notifier);
2222
case IEnumerable _ when itemsSource is INotifyCollectionChanged:
23-
return new ObservableItemsSource(itemsSource as IEnumerable, container, notifier);
23+
return new ObservableItemsSource(itemsSource, container, notifier);
2424
case IList list:
2525
return new ListSource(list);
2626
case IEnumerable<object> generic:

src/Controls/src/Core/Handlers/Items/Android/ItemsSources/ObservableItemsSource.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ internal class ObservableItemsSource : IItemsViewSource, IObservableItemsViewSou
1010
readonly IEnumerable _itemsSource;
1111
readonly BindableObject _container;
1212
readonly ICollectionChangedNotifier _notifier;
13+
readonly WeakNotifyCollectionChangedProxy _proxy = new();
14+
readonly NotifyCollectionChangedEventHandler _collectionChanged;
1315
bool _disposed;
1416

17+
~ObservableItemsSource() => _proxy.Unsubscribe();
18+
1519
public ObservableItemsSource(IEnumerable itemSource, BindableObject container, ICollectionChangedNotifier notifier)
1620
{
17-
_itemsSource = itemSource as IList ?? itemSource as IEnumerable;
21+
_itemsSource = itemSource;
1822
_container = container;
1923
_notifier = notifier;
20-
21-
((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
24+
_collectionChanged = CollectionChanged;
25+
_proxy.Subscribe((INotifyCollectionChanged)itemSource, _collectionChanged);
2226
}
2327

2428

src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public partial class CarouselViewHandler : ItemsViewHandler<CarouselView>
2323
WScrollBarVisibility? _horizontalScrollBarVisibilityWithoutLoop;
2424
WScrollBarVisibility? _verticalScrollBarVisibilityWithoutLoop;
2525
Size _currentSize;
26+
NotifyCollectionChangedEventHandler _collectionChanged;
27+
readonly WeakNotifyCollectionChangedProxy _proxy = new();
28+
29+
~CarouselViewHandler() => _proxy.Unsubscribe();
2630

2731
protected override IItemsLayout Layout { get; }
2832

@@ -47,9 +51,7 @@ protected override void DisconnectHandler(ListViewBase platformView)
4751
if (ListViewBase != null)
4852
{
4953
ListViewBase.SizeChanged -= OnListViewSizeChanged;
50-
51-
if (CollectionViewSource?.Source is ObservableItemTemplateCollection observableItemsSource)
52-
observableItemsSource.CollectionChanged -= OnCollectionItemsSourceChanged;
54+
_proxy.Unsubscribe();
5355
}
5456

5557
if (_scrollViewer != null)
@@ -120,7 +122,10 @@ protected override CollectionViewSource CreateCollectionViewSource()
120122
GetItemHeight(), GetItemWidth(), GetItemSpacing(), MauiContext);
121123

122124
if (collectionViewSource is ObservableItemTemplateCollection observableItemsSource)
123-
observableItemsSource.CollectionChanged += OnCollectionItemsSourceChanged;
125+
{
126+
_collectionChanged ??= OnCollectionItemsSourceChanged;
127+
_proxy.Subscribe(observableItemsSource, _collectionChanged);
128+
}
124129

125130
return new CollectionViewSource
126131
{

0 commit comments

Comments
 (0)