From 3e7e52246306c49008b94f3fee4a54a733e54533 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 16 Feb 2023 10:22:21 -0600 Subject: [PATCH] [controls] fix memory leak in `Window` Fixes: https://github.com/dotnet/maui/issues/10029 I could reproduce a memory leak by doing: App.Current.OpenWindow(new Window { Page = new ContentPage() }); I found that `App.Current._requestedWindows` just held onto every `Window` object forever. This is a perfect example of where using `ConditionalWeakTable` solves the issue: https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2 The only other change I made was to use `Guid.ToString("n")` as it removes `{`, `}`, and `-` characters from the string. After these changes, I still found `Window` objects hanging around that appeared to be held onto by many other objects. Then I reviewed: void IWindow.Destroying() { SendWindowDisppearing(); Destroying?.Invoke(this, EventArgs.Empty); OnDestroying(); Application?.RemoveWindow(this); // This wasn't here! Handler?.DisconnectHandler(); } It appears that upon `Window`'s closing, we didn't have any code that "disconnected" the MAUI handler. After this change, I could see `Window` objects properly go away. I added a unit test as well. --- .../Application/Application.Impl.cs | 11 +++++--- .../Core/HandlerImpl/Window/Window.Impl.cs | 1 + .../tests/Core.UnitTests/WindowsTests.cs | 25 ++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs b/src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs index f7edda731a9d..3f64527b0e53 100644 --- a/src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/Application/Application.Impl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Devices; @@ -12,7 +13,7 @@ public partial class Application : IApplication const string MauiWindowIdKey = "__MAUI_WINDOW_ID__"; readonly List _windows = new(); - readonly Dictionary _requestedWindows = new(); + readonly ConditionalWeakTable _requestedWindows = new(); ILogger? _logger; ILogger? Logger => @@ -30,7 +31,10 @@ IWindow IApplication.CreateWindow(IActivationState? activationState) if (activationState?.State?.TryGetValue(MauiWindowIdKey, out var requestedWindowId) ?? false) { if (requestedWindowId != null && _requestedWindows.TryGetValue(requestedWindowId, out var w)) + { window = w; + _requestedWindows.Remove(requestedWindowId); + } } // create a new one if there is no pending windows @@ -86,9 +90,8 @@ internal void RemoveWindow(Window window) public virtual void OpenWindow(Window window) { - var id = Guid.NewGuid().ToString(); - - _requestedWindows[id] = window; + var id = Guid.NewGuid().ToString("n"); + _requestedWindows.Add(id, window); var state = new PersistedState { diff --git a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs index ef7ee32bb343..286d0be07ee4 100644 --- a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs @@ -498,6 +498,7 @@ void IWindow.Destroying() OnDestroying(); Application?.RemoveWindow(this); + Handler?.DisconnectHandler(); } void IWindow.Resumed() diff --git a/src/Controls/tests/Core.UnitTests/WindowsTests.cs b/src/Controls/tests/Core.UnitTests/WindowsTests.cs index 6b063e341ae2..b59c42707f7c 100644 --- a/src/Controls/tests/Core.UnitTests/WindowsTests.cs +++ b/src/Controls/tests/Core.UnitTests/WindowsTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Maui.Graphics; @@ -621,5 +622,27 @@ public void MinDimensionsArePassedToCoreCorrectly(double inW, double inH, double Assert.Equal(outW, coreWindow.MinimumWidth); Assert.Equal(outH, coreWindow.MinimumHeight); } + + [Fact] + public async Task WindowDoesNotLeak() + { + var application = new Application(); + WeakReference reference; + + // Scope for window + { + var window = new Window { Page = new ContentPage() }; + reference = new WeakReference(window); + application.OpenWindow(window); + ((IWindow)window).Destroying(); + } + + // GC + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.False(reference.IsAlive, "Window should not be alive!"); + } } }