Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Fixes memory leak from grouped ListView with HasUnevenRows set #12447

Merged
merged 12 commits into from
Jul 6, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<controls:TestContentPage xmlns:controls="clr-namespace:Xamarin.Forms.Controls"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
x:Class="Xamarin.Forms.Controls.Issues.Issue6742">
<StackLayout>
<Button AutomationId="Issue6742Refresh" Clicked="RefreshItems" Text="Refresh" BackgroundColor="Cyan"/>

<Label Text="Tap Refresh button a few times." />
<Label AutomationId="Issue6742Label" Text="{Binding DisposeString}"/>
<Label AutomationId="Issue6742Mem" Text="{Binding TotalMemory}"/>

<ListView
HasUnevenRows="True"
ItemsSource="{Binding ItemGroups}"
IsGroupingEnabled="True"
GroupDisplayBinding="{Binding GroupName}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="20">
<Label Text="{Binding Name}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</controls:TestContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.ComponentModel;
using System.Collections.Generic;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Controls.Issues
{
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 6742, "Memory leak in ListView with uneven rows on IOS", PlatformAffected.Default)]
public partial class Issue6742 : TestContentPage
{
public Issue6742()
{
#if APP
InitializeComponent();
#endif
BindingContext = viewModel = new ViewModelIssue6742();

}
protected override void Init()
{

}
ViewModelIssue6742 viewModel;
IList<WeakReference<ModelIssue6742>> _weakList = new List<WeakReference<ModelIssue6742>>();
private void RefreshItems(object sender, EventArgs e)
{
foreach (var item in this.viewModel.ItemGroups)
{
item.Clear();
}
viewModel.ItemGroups.Clear();

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

CleanWeakList(_weakList);
viewModel.DisposeString = $"{_weakList.Count} object(s) alive";

string report = $"MEMORY:{GC.GetTotalMemory(true)}";
viewModel.TotalMemory = report;

var g1 = new ModelIssue6742Group("Group 1")
{
new ModelIssue6742("first", viewModel),
new ModelIssue6742("second", viewModel),
};

_weakList.Add(new WeakReference<ModelIssue6742>(g1[0]));
_weakList.Add(new WeakReference<ModelIssue6742>(g1[1]));

viewModel.ItemGroups.Add(g1);

var g2 = new ModelIssue6742Group("Group 2")
{
new ModelIssue6742("third", viewModel),
new ModelIssue6742("fourth", viewModel),
};

_weakList.Add(new WeakReference<ModelIssue6742>(g2[0]));
_weakList.Add(new WeakReference<ModelIssue6742>(g2[1]));

viewModel.ItemGroups.Add(g2);

var g3 = new ModelIssue6742Group("Group 3")
{
new ModelIssue6742("fifth", viewModel),
new ModelIssue6742("sixth", viewModel),
new ModelIssue6742("seventh", viewModel),
};

_weakList.Add(new WeakReference<ModelIssue6742>(g3[0]));
_weakList.Add(new WeakReference<ModelIssue6742>(g3[1]));
_weakList.Add(new WeakReference<ModelIssue6742>(g3[2]));

viewModel.ItemGroups.Add(g3);
}

private void CleanWeakList(IList<WeakReference<ModelIssue6742>> weakList)
{
ModelIssue6742 item;
for (int i = weakList.Count-1; i >= 0; i--)
{
if (!weakList[i].TryGetTarget(out item))
{
weakList.RemoveAt(i);
}
}
}
}

[Preserve(AllMembers = true)]
public class ViewModelIssue6742 : INotifyPropertyChanged
{
public ObservableCollection<ModelIssue6742Group> ItemGroups { get; set; } = new ObservableCollection<ModelIssue6742Group>();
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}


string _DisposeString;
public string DisposeString
{
get { return _DisposeString; }
set
{
_DisposeString = value;
NotifyPropertyChanged();
}
}

string _totalMemory;
public string TotalMemory
{
get => _totalMemory;
set
{
_totalMemory = value;
NotifyPropertyChanged();
}
}

public ViewModelIssue6742()
{

}
}

[Preserve(AllMembers = true)]
public class ModelIssue6742Group : ObservableCollection<ModelIssue6742>
{
public ModelIssue6742Group(string name)
{
GroupName = name;
}
public string GroupName { get; set; }

}

[Preserve(AllMembers = true)]
public class ModelIssue6742
{
ViewModelIssue6742 _model;

public ModelIssue6742(string name, ViewModelIssue6742 model)
{
_model = model;
Name = name;
}
public string Name { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
<DependentUpon>Issue11794.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Issue11769.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue6742.xaml.cs">
<DependentUpon>Issue6742.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Issue12079.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue12060.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue12344.xaml.cs">
Expand Down Expand Up @@ -2582,6 +2585,10 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue6742.xaml">
t-johnson marked this conversation as resolved.
Show resolved Hide resolved
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue10897.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
Expand Down
10 changes: 8 additions & 2 deletions Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@ nfloat GetEstimatedRowHeight(UITableView table)

var firstCell = templatedItems.ActivateContent(0, item);

nfloat returnValue;
// Let's skip this optimization for grouped lists. It will likely cause more trouble than it's worth.
if (firstCell?.Height > 0 && !isGroupingEnabled)
{
Expand All @@ -867,10 +868,15 @@ nfloat GetEstimatedRowHeight(UITableView table)
// In this case, we will cache the specified cell heights asynchronously, which will be returned one time on
// table load by EstimatedHeight.

return 0;
returnValue= 0;
}
else
{
returnValue = CalculateHeightForCell(table, firstCell);
}

return CalculateHeightForCell(table, firstCell);
TemplatedItemsView.UnhookContent(firstCell);
return returnValue;
}

internal override void InvalidatingPrototypicalCellCache()
Expand Down