From d59f34f274f1d1b75905461b3b1698ea2c1bc651 Mon Sep 17 00:00:00 2001 From: Daniel Kraut <33424430+danielkraut@users.noreply.github.com> Date: Fri, 7 Jan 2022 16:18:21 +0100 Subject: [PATCH] [Android] Fix content pages not Garbage collected (#14717) * Create test to prove that navigated-from content pages are not garbage collected when using Shell issue #14657 * Fix disposing Shell objects removing drawer listener (_drawerToggle) allows shell pages to be collected by GC issue #14657 * Correctly dispose views Shell TitleView is explicitly set to null, because otherwise pages would not be disposed of. Also views are explicitly removed during disposal. issue #14657 --- .../Issue14657.cs | 136 ++++++++++++++++++ ...rin.Forms.Controls.Issues.Shared.projitems | 1 + .../Renderers/ContainerView.cs | 1 + .../Renderers/ShellContentFragment.cs | 14 +- .../Renderers/ShellToolbarTracker.cs | 3 + 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue14657.cs diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue14657.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue14657.cs new file mode 100644 index 00000000000..442b68ff201 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue14657.cs @@ -0,0 +1,136 @@ +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; +using System; +using System.Threading; +using System.Threading.Tasks; + +#if UITEST +using Xamarin.Forms.Core.UITests; +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ +#if UITEST + [Category(UITestCategories.Github10000)] + [Category(UITestCategories.Shell)] + [Category(UITestCategories.TitleView)] +#endif + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 14657, "Shell pages are not released from memory", PlatformAffected.Android)] + public class Issue14657 : TestShell + { + static int pageCount = 0; + protected override void Init() + { + Routing.RegisterRoute(nameof(Issue14657_ChildPage), typeof(Issue14657_ChildPage)); + + var rootPage = CreateRootPage(); + + AddContentPage(rootPage); + } + + ContentPage CreateRootPage() + { + var rootPage = CreateContentPage("Home page"); + rootPage.Content = new StackLayout() + { + Children = + { + new Button() + { + Command = new Command(CollectMemory), + Text = "Force GC", + AutomationId = "GC_14657" + }, + new Button() + { + Command = new Command(async () => await GoToChild()), + Text = "Go to child page", + AutomationId = "GoToChild_14657" + } + } + }; + Shell.SetTitleView(rootPage, new StackLayout() + { + Orientation = StackOrientation.Horizontal, + Children = { new Label { Text = "Root Page" } } + }); + + return rootPage; + } + + public class Issue14657_ChildPage : ContentPage + { + public Issue14657_ChildPage() + { + Interlocked.Increment(ref pageCount); + + Content = new StackLayout + { + Children = + { + new Label() + { + Text = $"{pageCount}", + AutomationId = "CountLabel", + TextColor = Color.Black + }, + new Button() + { + Command = new Command(CollectMemory), + Text = "Force GC", + AutomationId = "GC_14657" + } + } + }; + } + + ~Issue14657_ChildPage() + { + Interlocked.Decrement(ref pageCount); + } + } + + async Task GoToChild() + { + await GoToAsync(nameof(Issue14657_ChildPage)); + } + + static void CollectMemory() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + +#if UITEST + [Test] + public void Issue14657Test() + { + RunningApp.Tap("GoToChild_14657"); + RunningApp.WaitForFirstElement("CountLabel") + .AssertHasText("1"); + RunningApp.NavigateBack(); + RunningApp.Tap("GC_14657"); + + RunningApp.Tap("GoToChild_14657"); + RunningApp.WaitForFirstElement("CountLabel") + .AssertHasText("1"); + RunningApp.NavigateBack(); + RunningApp.Tap("GC_14657"); + + RunningApp.Tap("GoToChild_14657"); + RunningApp.WaitForFirstElement("CountLabel") + .AssertHasText("1"); + RunningApp.NavigateBack(); + } + +#endif + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 94562258108..85e559fa6b5 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -973,6 +973,7 @@ Code + _TemplateMarkup.xaml Code diff --git a/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs b/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs index 8da11172705..85908ee7263 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs @@ -52,6 +52,7 @@ protected override void Dispose(bool disposing) if (disposing) { + RemoveAllViews(); _shellViewRenderer?.TearDown(); _view = null; } diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellContentFragment.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellContentFragment.cs index 75089926310..17329b00a93 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellContentFragment.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellContentFragment.cs @@ -165,6 +165,15 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, void Destroy() { + if (_page != null) + { + var titleView = Shell.GetTitleView(_page); + if (titleView != null) + { + Shell.SetTitleView(_page, null); + } + } + ((IShellController)_shellContext.Shell).RemoveAppearanceObserver(this); if (_shellContent != null) @@ -180,6 +189,8 @@ void Destroy() if (_root is ViewGroup vg) vg.RemoveView(_shellPageContainer); + + _shellPageContainer.Dispose(); } _renderer?.Dispose(); @@ -194,6 +205,7 @@ void Destroy() _root = null; _renderer = null; _shellContent = null; + _shellPageContainer = null; } protected override void Dispose(bool disposing) @@ -223,4 +235,4 @@ public override void OnDestroy() protected virtual void SetAppearance(ShellAppearance appearance) => _appearanceTracker.SetAppearance(_toolbar, _toolbarTracker, appearance); } -} \ No newline at end of file +} diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs index e0dde161f5d..606525d25f8 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs @@ -176,7 +176,10 @@ protected override void Dispose(bool disposing) _currentMenuItems?.Clear(); _currentToolbarItems?.Clear(); + _drawerLayout.RemoveDrawerListener(_drawerToggle); _drawerToggle?.Dispose(); + + _toolbar.RemoveAllViews(); } _currentMenuItems = null;