11

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...

9
  • What's the bitmap size of the 4K PNG's? PNG's are stored in memory without compression. It's possible to create a 4K PNG that's well over 1GB of data when converted to a bitmap. Also, yes, you really need to check whether the bitmaps are disposed. And probably the answer is, no they're not. Commented Sep 12, 2014 at 11:35
  • the PNG i'm currently using is defined as 512 x 512. Commented Sep 12, 2014 at 12:02
  • So that 4kB PNG requires 512 x 512 x 32 bits = 1MB of RAM to store/display. So it's very likely that you're indeed not disposing them. Commented Sep 12, 2014 at 12:04
  • Sorry, It's a tiny bit less. It's 24 bit depth... Commented Sep 12, 2014 at 12:15
  • 1
    I can confirm with certainty that Image Views loaded in a ViewCell gets 'NEVER' disposed. In contrast to a Image View placed on a Form. Tried and Tested. Nice Job! Xamarin Guys! Commented Sep 12, 2014 at 13:34

3 Answers 3

10

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

Sign up to request clarification or add additional context in comments.

11 Comments

Thanks for sharing your code and the explanation as well. just one thing, am I able to use an ImageCell with your code? I tried a custom ViewCell but couldn't get it to work. Cheers
Never tried with ImageCell. What's holding you back using a ViewCell? If you insist on using a ImageCell, then I guess would be a good idea changing the code in the 'GetContainingViewCell' method wherever you have 'ViewCell' to replace it with 'ImageCell'. I cannot see a reason it shouldn't do.
Even more, you could change 'ViewCell' just to 'Cell' so it will work with both.
I finally got my custom ViewCell to work, but I must be doing something else wrong as before I would get one call out of my list and then run out of memory but with the above code it runs out before it shows the list. I have no idea what I have done wrong
Can you show you code for the custom ViewCell, please?
|
2

Another set of steps that may help is the following:

There appears to be a memory leak on android involving listviews with custom cells. I did some investigating and found that if I did the following in my pages:

    protected override void OnAppearing()
    {
        BindingContext = controller.Model;
        base.OnAppearing();
    }

    protected override void OnDisappearing()
    {
        BindingContext = null;
        Content = null;
        base.OnDisappearing();
        GC.Collect();
    }

Set the android option in the manifest to use a large Heap (android:largeHeap="true") , and lastly, used the new Garbage collector, (in the environment.txt, MONO_GC_PARAMS=bridge-implementation=new) This combination of things, seems to fix the crashing issue. I can only assume that this is just because the setting of things to null, helps the GC to dispose of the elements, the large heap size to buy the GC time to do so, and the new GC option to help accelerate the collection itself. I sincerely hope this helps someone else...

3 Comments

I think you still have the memory leak. Solving things with largeHeap="true" is really a bad idea. It will work on devices with more memory but 'small' devices will not have extra memory available. You just postpone the crash.
Your correct, that was why I suggested that it was simply to buy time for the garbage collector to come through and free up space. I had a listview with multiple images and text per cell, displayed 5 to 10 cells on a screen at a time (depending on screen space), with several hundred part of the overall list, on a device with 256 MB of RAM, and it handled it without crashing. This was simply a band-aid until the Xamarin folks could get the memory leak fixed.
Where is environment.txt?
-3

In Visual Studio 2015 Go to Debug option>.droid Properties>Android Options>Advanced and set Java Max Heap Size to 1G

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.