diff --git a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs index 7444d8a533aa..7aa7f43dd426 100644 --- a/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/Window/Window.Impl.cs @@ -598,6 +598,7 @@ void OnPageChanged(Page? oldPage, Page? newPage) { if (oldPage != null) { + _menuBarTracker.Target = null; InternalChildren.Remove(oldPage); oldPage.HandlerChanged -= OnPageHandlerChanged; oldPage.HandlerChanging -= OnPageHandlerChanging; diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs index 87ab469ce799..8b1b54e28763 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs @@ -40,10 +40,10 @@ Task SetupWindowForTests(IWindow window, Func runTests, IMauiCon } finally { - window.Handler.DisconnectHandler(); + window.Handler?.DisconnectHandler(); await Task.Delay(10); newWindow?.Close(); - appStub.Handler.DisconnectHandler(); + appStub.Handler?.DisconnectHandler(); } }); } diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index a161651674f6..49dcc7d222a5 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -287,5 +287,28 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async }; }); } + + [Fact(DisplayName = "NavigationPage Does Not Leak")] + public async Task DoesNotLeak() + { + SetupBuilder(); + WeakReference pageReference = null; + var navPage = new NavigationPage(new ContentPage { Title = "Page 1" }); + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + var page = new ContentPage { Title = "Page 2" }; + pageReference = new WeakReference(page); + await navPage.Navigation.PushAsync(page); + await navPage.Navigation.PopAsync(); + }); + + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.NotNull(pageReference); + Assert.False(pageReference.IsAlive, "Page should not be alive!"); + } } } \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs index 319c0698c329..c62eb6174a72 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -660,7 +660,7 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } -#if !IOS +#if !IOS && !MACCATALYST [Fact] public async Task ChangingToNewMauiContextDoesntCrash() { @@ -917,6 +917,40 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async } #endif + [Fact(DisplayName = "Pages Do Not Leak")] + public async Task PagesDoNotLeak() + { + SetupBuilder(); + var shell = await CreateShellAsync(shell => + { + shell.CurrentItem = new ContentPage() { Title = "Page 1" }; + }); + + WeakReference pageReference = null; + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + + var page = new ContentPage { Title = "Page 2" }; + pageReference = new WeakReference(page); + + await shell.Navigation.PushAsync(page); + await shell.Navigation.PopAsync(); + }); + + // 3 GCs were required in Android API 23, 2 worked otherwise + for (int i = 0; i < 3; i++) + { + await Task.Yield(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + Assert.NotNull(pageReference); + Assert.False(pageReference.IsAlive, "Page should not be alive!"); + } + protected Task CreateShellAsync(Action action) => InvokeOnMainThreadAsync(() => { diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs index 61d5c550319e..45f89dba74ca 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.Android.cs @@ -52,6 +52,8 @@ public static void MapContent(IContentViewHandler handler, IContentView page) protected override void DisconnectHandler(ContentViewGroup platformView) { // If we're being disconnected from the xplat element, then we should no longer be managing its children + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; platformView.RemoveAllViews(); base.DisconnectHandler(platformView); } diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs index b56b6cfc28ed..b1303431bafc 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs @@ -49,5 +49,12 @@ public static void MapContent(IContentViewHandler handler, IContentView page) { UpdateContent(handler); } + + protected override void DisconnectHandler(ContentPanel platformView) + { + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; + base.DisconnectHandler(platformView); + } } } diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs index 9bebb97dec91..7447ff30ca8f 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.iOS.cs @@ -53,5 +53,15 @@ public static void MapContent(IContentViewHandler handler, IContentView page) { UpdateContent(handler); } + + protected override void DisconnectHandler(ContentView platformView) + { + platformView.CrossPlatformMeasure = null; + platformView.CrossPlatformArrange = null; + // TODO: this specific call fixes leaks in iOS/Catalyst + // But it doesn't feel quite right + platformView.RemoveFromSuperview(); + base.DisconnectHandler(platformView); + } } } diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index f8df45ab8851..97a25c1e7954 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -78,6 +78,14 @@ public override void OnResume() base.OnResume(); } + public override void OnDestroy() + { + _currentView = null; + _fragmentContainerView = null; + + base.OnDestroy(); + } + public override Animation OnCreateAnimation(int transit, bool enter, int nextAnim) { int id = 0; diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index f5ff35068b91..8ef5ce620062 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -10,6 +10,7 @@ override Microsoft.Maui.Handlers.EditorHandler.PlatformArrange(Microsoft.Maui.Gr override Microsoft.Maui.Handlers.RadioButtonHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect frame) -> void override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int +override Microsoft.Maui.Platform.NavigationViewFragment.OnDestroy() -> void override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool override Microsoft.Maui.SizeRequest.GetHashCode() -> int static Microsoft.Maui.FontSize.operator !=(Microsoft.Maui.FontSize left, Microsoft.Maui.FontSize right) -> bool diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index af7defd76aa7..8100e84655e4 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -7,6 +7,7 @@ Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTas Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.LifecycleEvents.iOSLifecycle.PerformFetch Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentView! platformView) -> void override Microsoft.Maui.Handlers.SwipeItemButton.Frame.get -> CoreGraphics.CGRect override Microsoft.Maui.Handlers.SwipeItemButton.Frame.set -> void override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.ConnectHandler(UIKit.UIButton! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index caa978707724..d9e63403cb61 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -6,6 +6,7 @@ Microsoft.Maui.IApplication.UserAppTheme.get -> Microsoft.Maui.ApplicationModel. Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentView! platformView) -> void override Microsoft.Maui.Handlers.SwipeItemButton.Frame.get -> CoreGraphics.CGRect override Microsoft.Maui.Handlers.SwipeItemButton.Frame.set -> void override Microsoft.Maui.Handlers.SwipeItemMenuItemHandler.ConnectHandler(UIKit.UIButton! platformView) -> void diff --git a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 4653108a9e81..325d7513e50a 100644 --- a/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -4,6 +4,7 @@ Microsoft.Maui.Hosting.MauiApp.DisposeAsync() -> System.Threading.Tasks.ValueTas Microsoft.Maui.Layouts.FlexBasis.Equals(Microsoft.Maui.Layouts.FlexBasis other) -> bool Microsoft.Maui.Platform.MauiWebView.MauiWebView(Microsoft.Maui.Handlers.WebViewHandler! handler) -> void Microsoft.Maui.SizeRequest.Equals(Microsoft.Maui.SizeRequest other) -> bool +override Microsoft.Maui.Handlers.ContentViewHandler.DisconnectHandler(Microsoft.Maui.Platform.ContentPanel! platformView) -> void override Microsoft.Maui.Layouts.FlexBasis.Equals(object? obj) -> bool override Microsoft.Maui.Layouts.FlexBasis.GetHashCode() -> int override Microsoft.Maui.SizeRequest.Equals(object? obj) -> bool