Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ios/catalyst] fix memory leaks in *Cell #22067

Merged
merged 8 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class CellTableViewCell : UITableViewCell, INativeElementView
{
Cell _cell;
WeakReference<Cell> _cell;
readonly WeakEventManager _weakEventManager = new();

// NOTE: internal callers can use InternalPropertyChanged
[Obsolete("To be removed in a future release.")]
public Action<object, PropertyChangedEventArgs> PropertyChanged;

internal event Action<object, PropertyChangedEventArgs> InternalPropertyChanged
{
add => _weakEventManager.AddEventHandler(value);
remove => _weakEventManager.RemoveEventHandler(value);
}

bool _disposed;

public CellTableViewCell(UITableViewCellStyle style, string key) : base(style, key)
Expand All @@ -21,30 +30,37 @@ public CellTableViewCell(UITableViewCellStyle style, string key) : base(style, k

public Cell Cell
{
get { return _cell; }
get => _cell?.GetTargetOrDefault();
set
{
if (_cell == value)
return;

if (_cell != null)
if (_cell is not null)
{
_cell.PropertyChanged -= HandlePropertyChanged;
BeginInvokeOnMainThread(_cell.SendDisappearing);
if (_cell.TryGetTarget(out var cell) && cell == value)
return;

if (cell is not null)
{
cell.PropertyChanged -= HandlePropertyChanged;
BeginInvokeOnMainThread(cell.SendDisappearing);
}
}
_cell = value;

if (_cell != null)
if (value is not null)
{
_cell.PropertyChanged += HandlePropertyChanged;
BeginInvokeOnMainThread(_cell.SendAppearing);
_cell = new(value);
value.PropertyChanged += HandlePropertyChanged;
BeginInvokeOnMainThread(value.SendAppearing);
}
else
{
_cell = null;
}
}
}

public Element Element => Cell;

public void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(sender, e);
public void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) => _weakEventManager.HandleEvent(sender, e, nameof(InternalPropertyChanged));

internal static UITableViewCell GetPlatformCell(UITableView tableView, Cell cell, bool recycleCells = false, string templateId = "")
{
Expand Down Expand Up @@ -109,12 +125,10 @@ protected override void Dispose(bool disposing)

if (disposing)
{
PropertyChanged = null;

if (_cell != null)
if (Cell is Cell cell)
{
_cell.PropertyChanged -= HandlePropertyChanged;
CellRenderer.SetRealCell(_cell, null);
cell.PropertyChanged -= HandlePropertyChanged;
CellRenderer.SetRealCell(cell, null);
}
_cell = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell,
tvc = new EntryCellTableViewCell(item.GetType().FullName);
else
{
tvc.PropertyChanged -= HandlePropertyChanged;
tvc.InternalPropertyChanged -= HandlePropertyChanged;
tvc.TextFieldTextChanged -= OnTextFieldTextChanged;
tvc.KeyboardDoneButtonPressed -= OnKeyBoardDoneButtonPressed;
}

SetRealCell(item, tvc);

tvc.Cell = item;
tvc.PropertyChanged += HandlePropertyChanged;
tvc.InternalPropertyChanged += HandlePropertyChanged;
tvc.TextFieldTextChanged += OnTextFieldTextChanged;
tvc.KeyboardDoneButtonPressed += OnKeyBoardDoneButtonPressed;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ void SetImage(ImageCell cell, CellTableViewCell target)

source.LoadImage(cell.FindMauiContext(), (result) =>
{
var uiimage = result.Value;
if (uiimage != null)
var uiimage = result?.Value;
if (uiimage is not null)
{
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
{
if (target.Cell != null)
if (target.Cell is not null)
{
target.ImageView.Image = uiimage;
target.SetNeedsLayout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell,
else
{
uiSwitch = tvc.AccessoryView as UISwitch;
tvc.PropertyChanged -= HandlePropertyChanged;
tvc.InternalPropertyChanged -= HandlePropertyChanged;
}

SetRealCell(item, tvc);
Expand All @@ -45,7 +45,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell,
_defaultOnColor = UISwitch.Appearance.OnTintColor;

tvc.Cell = item;
tvc.PropertyChanged += HandlePropertyChanged;
tvc.InternalPropertyChanged += HandlePropertyChanged;
tvc.AccessoryView = uiSwitch;
#pragma warning disable CA1416, CA1422 // TODO: 'UITableViewCell.TextLabel' is unsupported on: 'ios' 14.0 and later
tvc.TextLabel.Text = boolCell.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell,
if (!(reusableCell is CellTableViewCell tvc))
tvc = new CellTableViewCell(UITableViewCellStyle.Subtitle, item.GetType().FullName);
else
tvc.PropertyChanged -= HandleCellPropertyChanged;
tvc.InternalPropertyChanged -= HandleCellPropertyChanged;

SetRealCell(item, tvc);

tvc.Cell = textCell;
tvc.PropertyChanged = HandleCellPropertyChanged;
tvc.InternalPropertyChanged += HandleCellPropertyChanged;

#pragma warning disable CA1416, CA1422 // TODO: 'UITableViewCell.TextLabel', DetailTextLabel is unsupported on: 'ios' 14.0 and later
tvc.TextLabel.Text = textCell.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell,

internal sealed class ViewTableCell : UITableViewCell, INativeElementView
{
IMauiContext MauiContext => _viewCell.FindMauiContext();
IMauiContext MauiContext => ViewCell?.FindMauiContext();
WeakReference<IPlatformViewHandler> _rendererRef;
ViewCell _viewCell;
WeakReference<ViewCell> _viewCell;

Element INativeElementView.Element => ViewCell;
internal bool SupressSeparator { get; set; }
Expand All @@ -57,13 +57,15 @@ public ViewTableCell(string key) : base(UITableViewCellStyle.Default, key)

public ViewCell ViewCell
{
get { return _viewCell; }
get => _viewCell?.GetTargetOrDefault();
set
{
if (_viewCell == value)
return;

_viewCell?.Handler?.DisconnectHandler();
if (_viewCell is not null)
{
if (_viewCell.TryGetTarget(out var viewCell) && viewCell == value)
return;
viewCell?.Handler?.DisconnectHandler();
}
UpdateCell(value);
}
}
Expand All @@ -82,7 +84,7 @@ void ViewCellPropertyChanged(object sender, PropertyChangedEventArgs e)
var realCell = (ViewTableCell)GetRealCell(viewCell);

if (e.PropertyName == Cell.IsEnabledProperty.PropertyName)
UpdateIsEnabled(_viewCell.IsEnabled);
UpdateIsEnabled(viewCell.IsEnabled);
}

public override void LayoutSubviews()
Expand Down Expand Up @@ -149,11 +151,11 @@ protected override void Dispose(bool disposing)
_rendererRef = null;
}

if (_viewCell != null)
if (ViewCell is ViewCell viewCell)
{
_viewCell.PropertyChanged -= ViewCellPropertyChanged;
_viewCell.View.MeasureInvalidated -= OnMeasureInvalidated;
SetRealCell(_viewCell, null);
viewCell.PropertyChanged -= ViewCellPropertyChanged;
viewCell.View.MeasureInvalidated -= OnMeasureInvalidated;
SetRealCell(viewCell, null);
}
_viewCell = null;
}
Expand All @@ -165,10 +167,10 @@ protected override void Dispose(bool disposing)

IPlatformViewHandler GetNewRenderer()
{
if (_viewCell.View == null)
throw new InvalidOperationException($"ViewCell must have a {nameof(_viewCell.View)}");
if (ViewCell is not ViewCell viewCell || viewCell.View == null)
throw new InvalidOperationException($"ViewCell must have a {nameof(viewCell.View)}");

var newRenderer = _viewCell.View.ToHandler(_viewCell.View.FindMauiContext());
var newRenderer = viewCell.View.ToHandler(viewCell.View.FindMauiContext());
_rendererRef = new WeakReference<IPlatformViewHandler>(newRenderer);
ContentView.ClearSubviews();
ContentView.AddSubview(newRenderer.VirtualView.ToPlatform());
Expand All @@ -179,37 +181,37 @@ void UpdateCell(ViewCell cell)
{
Performance.Start(out string reference);

var oldCell = _viewCell;
if (oldCell != null)
if (ViewCell is ViewCell oldCell)
{
BeginInvokeOnMainThread(oldCell.SendDisappearing);
oldCell.PropertyChanged -= ViewCellPropertyChanged;
oldCell.View.MeasureInvalidated -= OnMeasureInvalidated;
}

_viewCell = cell;

if (cell is null)
{
_viewCell = null;
_rendererRef = null;
ContentView.ClearSubviews();
return;
}

_viewCell.PropertyChanged += ViewCellPropertyChanged;
BeginInvokeOnMainThread(_viewCell.SendAppearing);
_viewCell = new(cell);

cell.PropertyChanged += ViewCellPropertyChanged;
BeginInvokeOnMainThread(cell.SendAppearing);

IPlatformViewHandler renderer;
if (_rendererRef == null || !_rendererRef.TryGetTarget(out renderer))
renderer = GetNewRenderer();
else
{
var viewHandlerType = MauiContext.Handlers.GetHandlerType(_viewCell.View.GetType());
var viewHandlerType = MauiContext.Handlers.GetHandlerType(cell.View.GetType());
var reflectableType = renderer as System.Reflection.IReflectableType;
var rendererType = reflectableType != null ? reflectableType.GetTypeInfo().AsType() : (renderer != null ? renderer.GetType() : typeof(System.Object));

if (rendererType == viewHandlerType/* || (renderer is Platform.DefaultRenderer && type == null)*/)
renderer.SetVirtualView(this._viewCell.View);
renderer.SetVirtualView(cell.View);
else
{
//when cells are getting reused the element could be already set to another cell
Expand All @@ -219,8 +221,8 @@ void UpdateCell(ViewCell cell)
}
}

UpdateIsEnabled(_viewCell.IsEnabled);
_viewCell.View.MeasureInvalidated += OnMeasureInvalidated;
UpdateIsEnabled(cell.IsEnabled);
cell.View.MeasureInvalidated += OnMeasureInvalidated;
Performance.Stop(reference);
}

Expand Down
47 changes: 47 additions & 0 deletions src/Controls/tests/DeviceTests/Memory/MemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ void SetupBuilder()
handlers.AddHandler<CheckBox, CheckBoxHandler>();
handlers.AddHandler<DatePicker, DatePickerHandler>();
handlers.AddHandler<Entry, EntryHandler>();
handlers.AddHandler<EntryCell, EntryCellRenderer>();
handlers.AddHandler<Editor, EditorHandler>();
handlers.AddHandler<Frame, FrameRenderer>();
handlers.AddHandler<GraphicsView, GraphicsViewHandler>();
Expand All @@ -41,6 +42,7 @@ void SetupBuilder()
handlers.AddHandler<IContentView, ContentViewHandler>();
handlers.AddHandler<Image, ImageHandler>();
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<ImageCell, ImageCellRenderer>();
handlers.AddHandler<IndicatorView, IndicatorViewHandler>();
handlers.AddHandler<RefreshView, RefreshViewHandler>();
handlers.AddHandler<IScrollView, ScrollViewHandler>();
Expand All @@ -49,11 +51,13 @@ void SetupBuilder()
handlers.AddHandler<Stepper, StepperHandler>();
handlers.AddHandler<SwipeView, SwipeViewHandler>();
handlers.AddHandler<Switch, SwitchHandler>();
handlers.AddHandler<SwitchCell, SwitchCellRenderer>();
handlers.AddHandler<TableView, TableViewRenderer>();
handlers.AddHandler<TextCell, TextCellRenderer>();
handlers.AddHandler<TimePicker, TimePickerHandler>();
handlers.AddHandler<Toolbar, ToolbarHandler>();
handlers.AddHandler<WebView, WebViewHandler>();
handlers.AddHandler<ViewCell, ViewCellRenderer>();
#if IOS || MACCATALYST
handlers.AddHandler<NavigationPage, NavigationRenderer>();
#else
Expand Down Expand Up @@ -242,6 +246,49 @@ public async Task GestureDoesNotLeak(Type type)
await AssertionExtensions.WaitForGC(viewReference, handlerReference);
}

[Theory("Cells Do Not Leak")]
[InlineData(typeof(TextCell))]
[InlineData(typeof(EntryCell))]
[InlineData(typeof(ImageCell))]
[InlineData(typeof(SwitchCell))]
[InlineData(typeof(ViewCell))]
public async Task CellsDoNotLeak(Type type)
{
SetupBuilder();

WeakReference viewReference = null;
WeakReference handlerReference = null;

var observable = new ObservableCollection<int> { 1 };
var navPage = new NavigationPage(new ContentPage { Title = "Page 1" });

await CreateHandlerAndAddToWindow(new Window(navPage), async () =>
{
await navPage.Navigation.PushAsync(new ContentPage
{
Content = new ListView
{
ItemTemplate = new DataTemplate(() =>
{
var cell = (Cell)Activator.CreateInstance(type);
if (cell is ViewCell viewCell)
{
viewCell.View = new Label();
}
viewReference = new WeakReference(cell);
handlerReference = new WeakReference(cell.Handler);
return cell;
}),
ItemsSource = observable
}
});

await navPage.Navigation.PopAsync();
});

await AssertionExtensions.WaitForGC(viewReference, handlerReference);
}

#if IOS
[Fact]
public async Task ResignFirstResponderTouchGestureRecognizer()
Expand Down