Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this a memory leak in Xamarin Forms?

I have run into a problem where it appears Page objects are not being Garbage Collected once they have been navigated away from. I have put together a very basic example of this that demonstrates the issue when using a NavigationPage and the PushAsync method. The page displays the number of 'Alive' pages using a list of weak references:

public class AppNavigationPage
{
    private static List<WeakReference> pageRefs = new List<WeakReference>();

    public static Page GetMainPage()
    {
        return new NavigationPage(CreateWeakReferencedPage());
    }

    private static Page CreateWeakReferencedPage()
    {
        GC.Collect();
        var result = CreatePage();
        pageRefs.Add(new WeakReference(result));

        // Add a second unreferenced page to prove that the problem only exists
        // when pages are actually navigated to/from
        pageRefs.Add(new WeakReference(CreatePage()));
        GC.Collect();
        return result;
    }

    private static Page CreatePage()
    {
        var page = new ContentPage();
        var contents = new StackLayout();

        contents.Children.Add(
            new Button
            {
                Text = "Next Page",
                Command = new Command(() => page.Navigation.PushAsync(CreateWeakReferencedPage()))
            });
        contents.Children.Add(
            new Label
            {
                Text = string.Format(
                    "References alive at time of creation: {0}",
                    pageRefs.Count(p => p.IsAlive)),
                HorizontalOptions = LayoutOptions.CenterAndExpand
            });

        page.Content = contents;
        return page;
    }
}

As you click the Next Page button, a new page is created with a fixed value label showing the number of page references alive at the point this page was created. Each time you click the button you obviously see this number increase by 1. My understanding is that when you click 'back' on the Navigation Page, the view should be popped off the stack and thrown away (allowing it to be GC'd). However, when I run this test code it indicates that after we have gone back, this view is being retained in memory. This can be demonstrated by clicking Next Page a few times until the reference count is at 3. If you then click Back and then Next Page, I believe the reference count should still be 3 (indicating the old page was GC'd before the new one was created) however the new reference count is now 4.

This seems like quite a serious bug in the X-Forms navigation implementation for iOS (I haven't tested this for other platforms), my guess being it is somehow related to the Strong Reference Cycle problem described here: http://developer.xamarin.com/guides/cross-platform/application_fundamentals/memory_perf_best_practices/

Has anyone else encountered this and/or come up with a solution/workaround for it? Would anyone else agree this is a bug?

As an addition, I did a second example that doesn't involve a NavigationPage (so has to use PushModalAsync instead) and found I had the same problem, so this issue doesn't look to be unique to NavigationPage navigation. For reference the code for that (very similar) test is here:

public class AppModal
{
    private static List<WeakReference> pageRefs = new List<WeakReference>();

    public static Page GetMainPage()
    {
        return CreateWeakReferencedPage();
    }

    private static Page CreateWeakReferencedPage()
    {
        GC.Collect();
        var result = CreatePage();
        pageRefs.Add(new WeakReference(result));

        // Add a second unreferenced page to prove that the problem only exists
        // when pages are actually navigated to/from
        pageRefs.Add(new WeakReference(CreatePage()));
        GC.Collect();
        return result;
    }

    private static Page CreatePage()
    {
        var page = new ContentPage();
        var contents = new StackLayout();

        contents.Children.Add(
            new Button
            {
                Text = "Next Page",
                Command = new Command(() => page.Navigation.PushModalAsync(CreateWeakReferencedPage()))
            });
        contents.Children.Add(
            new Button
            {
                Text = "Close",
                Command = new Command(() => page.Navigation.PopModalAsync())
            });
        contents.Children.Add(
            new Label
            {
                Text = string.Format(
                    "References alive at time of creation: {0}",
                    pageRefs.Count(p => p.IsAlive)),
                HorizontalOptions = LayoutOptions.CenterAndExpand
            });

        page.Content = contents;
        return page;
    }
}
like image 482
tonyj444 Avatar asked Aug 08 '14 08:08

tonyj444


People also ask

How do you identify a memory leak?

The system can have a myriad of symptoms that point to a leak, though: decreased performance, a slowdown plus the inability to open additional programs, or it may freeze up completely.

How do I know if my app has a memory leak?

Memory profilers are tools that can monitor memory usage and help detect memory leaks in an application. Profilers can also help with analyzing how resources are allocated within an application, for example how much memory and CPU time is being used by each method. This can help identify and narrow down any issues.

What is memory leak in xamarin?

With incorrect memory management, an object is stored in memory but cannot be accessed by the running code, it's always causing a memory leak in mobile application.


Video Answer


1 Answers

I think what you are seeing is a side effect of async navigation, not memory leak. Instead of WeakReferences you might opt for a finalizer instead and create instances of MyPage (instead of ContentPage).

    public class MyPage: ContentPage
    {
        private static int count;

        public MyPage()
        {
            count++;
            Debug.WriteLine("Created total " + count);
        }
        ~MyPage()
        {
            count--;
            Debug.WriteLine("Finalizer, remaining " + count);
        }
    }

Next trick is to add a delayed GC.Collect() call, like:

    private static Page CreateWeakReferencedPage()
    {
        GC.Collect();
        var result = CreatePage();
        var ignore = DelayedGCAsync();
        return result;
    }

    private static async Task DelayedGCAsync()
    {
        await Task.Delay(2000);
        GC.Collect();
    }

You will note that instances get garbage collected within this delayed collection (output window). As per Xamarin GarbageCollector: I doubt that it has serious flaws. A minor bug here and there but not that huge. That said, dealing with garbage collections in Android is particularly tricky because there are two of those - Dalvik's and Xamarin's. But that is another story.

like image 97
Miha Markic Avatar answered Oct 04 '22 15:10

Miha Markic