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;