Avrohom Avrohom - 2 months ago 44
Android Question

Xamarin.Forms ListView OutOfMemoryError exception on Android

Anyone ever tried A Xamarin.Forms Listview with an ItemTemplate containing a Image view? Now, what happens when ListView contains ca 20 or more rows?

As for me, I have a .png file of around 4K in size loaded into the Image view. Got a maximum of 9 - 12 rows shown before application crashed with a OutOfMemoryError. After requesting a large heap in the android Manifest, the app crashes after 60 - 70 rows.

I know that Xamarin is promoting the use of the BitmapFactory class to scale down bitmaps, but this is not applicable (out of the box) for the Xamarin Forms Image View.

I'm about trying to fiddle with a Sub Class of the ImageRenderer to see if I can add a BitmapFactory.Options property and if this will solve the problem.

Also, I might need to check out if Xamarin.Forms does dispose (recycle) the contained bitmap after the ViewCell is being scrolled of the screen.

Before setting out on this journey, I would be very keen to get any comments that could make this easier or a simpler solution that would deem this process unnecessary.

Looking forward...

Answer

Yes, I found a solution. Code to follow. But before that, let me explain a bit what I have done.

So, there's definitely a need to take maters in our own hands to dispose the image and its underlying resources (bitmap or drawable, however you want to call it). Basically, it comes down to dispose the native 'ImageRenderer' object.

Now, there's no way to obtain a reference to that ImageRenderer from anywhere because in order to do so, one need to be able to call Platform.GetRenderer(...). Access to the 'Platform' class is inaccessible since its scope is declared as 'internal'.

So, I have been left with no choice other than to sub-class the Image class and its (Android) Renderer and destroy this Renderer itself from inside (passing 'true' as argument. Don't try with 'false'). Inside the Renderer I hook on to page disappear (In case of a TabbedPage). In most situations the Page Disappear event will not serve the purpose well, such as when the page is still in the screen stack but disappears due to a another page is being drawn on Top of it. If you dispose the Image(s) than, when the page gets uncovered (shown) again, it will not display the images. In such case we have to hook on the the main Navigation Page's 'Popped' event.

I have tried to explain to the best I could. The rest - I hope - you will be able to get from the code:

This is the Image Sub-Class in the PCL Project.

using System;

using Xamarin.Forms;

namespace ApplicationClient.CustomControls
{
    public class LSImage : Image
    {
    }
}

The following code is in the Droid project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Android.Widget;
using Android.Util;
using Application.Droid.CustomControls;
using ApplicationClient.CustomControls;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

    [assembly: ExportRenderer(typeof(ApplicationClient.CustomControls.LSImage), typeof(LSImageRenderer))]

    namespace Application.Droid.CustomControls
    {
        public class LSImageRenderer : ImageRenderer
        {
            Page page;
            NavigationPage navigPage;

            protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
            {
                base.OnElementChanged(e);
                if (e.OldElement == null)
                {
                    if (GetContainingViewCell(e.NewElement) != null)
                    {
                        page = GetContainingPage(e.NewElement);
                        if (page.Parent is TabbedPage)
                        {
                            page.Disappearing += PageContainedInTabbedPageDisapearing;
                            return;
                        }

                        navigPage = GetContainingNavigationPage(page);
                        if (navigPage != null)
                            navigPage.Popped += OnPagePopped;
                    }
                    else if ((page = GetContainingTabbedPage(e.NewElement)) != null)
                    {
                        page.Disappearing += PageContainedInTabbedPageDisapearing;
                    }
                }
            }

            void PageContainedInTabbedPageDisapearing (object sender, EventArgs e)
            {
                this.Dispose(true);
                page.Disappearing -= PageContainedInTabbedPageDisapearing;
            }

            protected override void Dispose(bool disposing)
            {
                Log.Info("**** LSImageRenderer *****", "Image got disposed");
                base.Dispose(disposing);
            }

            private void OnPagePopped(object s, NavigationEventArgs e)
            {
                if (e.Page == page)
                {
                    this.Dispose(true);
                    navigPage.Popped -= OnPagePopped;
                }
            }

            private Page GetContainingPage(Xamarin.Forms.Element element)
            {
                Element parentElement = element.ParentView;

                if (typeof(Page).IsAssignableFrom(parentElement.GetType()))
                    return (Page)parentElement;
                else
                    return GetContainingPage(parentElement);
            }

            private ViewCell GetContainingViewCell(Xamarin.Forms.Element element)
            {
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(ViewCell).IsAssignableFrom(parentElement.GetType()))
                    return (ViewCell)parentElement;
                else
                    return GetContainingViewCell(parentElement);
            }

            private TabbedPage GetContainingTabbedPage(Element element)
            {
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(TabbedPage).IsAssignableFrom(parentElement.GetType()))
                    return (TabbedPage)parentElement;
                else
                    return GetContainingTabbedPage(parentElement);
            }

            private NavigationPage GetContainingNavigationPage(Element element)
            {
                Element parentElement = element.Parent;

                if (parentElement == null)
                    return null;

                if (typeof(NavigationPage).IsAssignableFrom(parentElement.GetType()))
                    return (NavigationPage)parentElement;
                else
                    return GetContainingNavigationPage(parentElement);
            }
        }
    }

Finally, I have changed the Name of the Application in the namespace to 'ApplicationClient' in the PCL project and to 'Application.Droid' in Droid project. You should change it to your app name.

Also, the few recursive methods at the end of the Renderer class, I know that I could combine it into one Generic method. The thing is, that I have build one at a time as the need arose. So, this is how I left it.

Happy coding,

Avrohom

Comments