-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
[controls] fix memory leak with Grid Row/ColumnDefinitions #16145
Conversation
Context: dotnet#15860 In a customer's app, they have the setup: <!-- Resources/Styles/Styles.xaml --> <Style TargetType="Grid" x:Key="GridStyleWithColumnDefinitions"> <Setter Property="ColumnDefinitions" Value="18,*"/> </Style> Then a page with: <Grid Style="{StaticResource GridStyleWithColumnDefinitions}" /> Navigating forward & back from this page would show that the entire `Page` would live forever! What is interesting, is simply removing the Grid's `Style` solved the problem?!? I narrowed this down to a unit test: [Fact] public async Task ColumnDefinitionDoesNotLeak() { // Long-lived column, like from a Style in App.Resources var column = new ColumnDefinition(); WeakReference reference; { var grid = new Grid(); grid.ColumnDefinitions.Add(column); reference = new(grid); } await Task.Yield(); GC.Collect(); GC.WaitForPendingFinalizers(); Assert.False(reference.IsAlive, "Grid should not be alive!"); } https://github.com/dotnet/maui/blob/cda3eb3381cfb686567ed05e3eb8e8f26c02d785/src/Controls/src/Core/Layout/Grid.cs#L20 `Grid` subscribes to `DefinitionCollection<T>.ItemSizeChanged`, and in this case the `DefinitionCollection<T>` lived indefinitely in an application-wide `ResourceDictionary`. The simplest solution is to make the `ItemSizeChanged` event use `WeakEventManager` as was done for other events like `Application.RequestedThemeChanged`: https://github.com/dotnet/maui/blob/cda3eb3381cfb686567ed05e3eb8e8f26c02d785/src/Controls/src/Core/Application/Application.cs#L221-L225 Since MAUI owns the property, we can use the simple solution here. This is one issue solved in the customer app, but I will need to retest the app in dotnet#15860 to see if there are further issues.
public event EventHandler ItemSizeChanged; | ||
public event EventHandler ItemSizeChanged | ||
{ | ||
add => _weakEventManager.AddEventHandler(value); | ||
remove => _weakEventManager.RemoveEventHandler(value); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some existing tests that check this event still fires:
maui/src/Controls/tests/Core.UnitTests/Layouts/GridLayoutTests.cs
Lines 77 to 78 in cda3eb3
[Fact] | |
public void ChangingChildRowInvalidatesGrid() |
maui/src/Controls/tests/Core.UnitTests/Layouts/GridLayoutTests.cs
Lines 98 to 99 in cda3eb3
[Fact] | |
public void ChangingChildColumnInvalidatesGrid() |
I did some testing and I think this does actually fix: #15860 |
How does that work exactly? How does using WeakEventManager solve the issue of the DefinitionCollection living indefinitely? (not reviewing, just asking questions to learn) |
When you make a C# event: class Child {
public event EventHandler MyEvent; If you are inside a class child.MyEvent += this.OnMyEvent; // let's say this is inside the Parent's constructor
void OnMyEvent(object sender, EventArgs e) { } The
maui/src/Core/src/WeakEventManager.cs Line 157 in ed18303
A So now |
Interesting! Learned so much, thanks :)
Would you say this design was intentional or more of an oversight? Is there anything that is generally good about the child having a strong reference to the parent? |
I think C# events have been like this since .NET Framework 1.0. Back then there was only WinForms, no WPF or MVVM patterns. Most events were used for things like There is an api proposal about making "weak events" a thing: |
@rachelkang I wrote down some of the comments here for the future. Should be useful to us and customers: https://github.com/dotnet/maui/wiki/Memory-Leaks I didn't write a section about circular references in iOS/Catalyst yet. There is unfortunately more I need to add here... |
Fixes: #15860
Fixes: #15986
In a customer's app, they have the setup:
Then a page with:
Navigating forward & back from this page would show that the entire
Page
would live forever! What is interesting, is simply removing the Grid'sStyle
solved the problem?!?I narrowed this down to a unit test:
maui/src/Controls/src/Core/Layout/Grid.cs
Line 20 in cda3eb3
Grid
subscribes toDefinitionCollection<T>.ItemSizeChanged
, and in this case theDefinitionCollection<T>
lived indefinitely in an application-wideResourceDictionary
.The simplest solution is to make the
ItemSizeChanged
event useWeakEventManager
as was done for other events likeApplication.RequestedThemeChanged
:maui/src/Controls/src/Core/Application/Application.cs
Lines 221 to 225 in cda3eb3
Since MAUI owns the property, we can use the simple solution here.
This is one issue solved in the customer app, but I will need to retest the app in #15860 to see if there are further issues.