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

gonzobrains
  • 7,856
  • 14
  • 81
  • 132
Avrohom
  • 640
  • 8
  • 23
  • 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. – Frank Sep 12 '14 at 11:35
  • the PNG i'm currently using is defined as 512 x 512. – Avrohom Sep 12 '14 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. – Frank Sep 12 '14 at 12:04
  • Sorry, It's a tiny bit less. It's 24 bit depth... – Avrohom Sep 12 '14 at 12:15
  • Afaik the default behavior is to decompress to 32 bits. Even if the source if only a black&white (1-bit) PNG. Either way, that's not really the issue. The issue is likely the not-recycling of the bitmaps. – Frank Sep 12 '14 at 12:19
  • 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! – Avrohom Sep 12 '14 at 13:34
  • The solution would then be to overwrite cell-reuse behavior and code it to dispose it. – Frank Sep 12 '14 at 14:23
  • Perhaps the images are being cached (not very well obviously) but it is really hard to judge without seeing the code (repro sample) and without knowing the environment (target OS and XF version). – Miha Markic Sep 20 '14 at 14:35
  • Did you find a solution? I am having the same issues. How would I overwrite the cell-reuse behaviour code? – user1667474 Sep 28 '14 at 08:45

3 Answers3

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

Avrohom
  • 640
  • 8
  • 23
  • 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 – user1667474 Sep 29 '14 at 13:35
  • 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. – Avrohom Sep 29 '14 at 16:12
  • Even more, you could change 'ViewCell' just to 'Cell' so it will work with both. – Avrohom Sep 29 '14 at 16:13
  • 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 – user1667474 Sep 30 '14 at 13:29
  • Can you show you code for the custom ViewCell, please? – Avrohom Sep 30 '14 at 16:45
  • Hi Avrohom. I have posted another question here (http://stackoverflow.com/questions/26253531/xamarin-forms-not-allowing-user-to-go-back-to-splash-screen) so that I could properly show the code – user1667474 Oct 08 '14 at 09:30
  • Nice solution, but does need a bit on tweaking in the `OnPagePopped` function to handle MasterDetailPage... for me the images in a `ListView` on the `Detail` page wasn't being disposed. – fredrik Mar 07 '15 at 15:37
  • Thank you for solution! Though it covers only "one half" of the problem, smaller one :) Memory in ListView is leaking while scrolling, as new cells are created and not recycled. And if you have 10-20+ items in list and they should be scrolled up and down (i.e. chat application), it goes bad very fast https://bugzilla.xamarin.com/show_bug.cgi?id=23807 – rudyryk May 10 '15 at 18:53
  • Could you please post the iOS renderer as well for noobs like me? – Amir Hajiha Jan 02 '19 at 17:28
  • Sorry @Amir No-Family, I have not implemented such on iOS. Are you sure that iOS it is also having memory leak? – Avrohom Jan 02 '19 at 21:27
  • I just don’t know how to create a custom renderer for Android and not create one for iOS. No there’s no leak on iOS. – Amir Hajiha Jan 03 '19 at 00:15
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...

TChadwick
  • 868
  • 10
  • 19
  • 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. – Ton Snoei Nov 28 '16 at 07:57
  • 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. – TChadwick Nov 29 '16 at 15:40
  • Where is environment.txt? – Amir Hajiha Jan 03 '19 at 00:13
-3

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

Adit Kothari
  • 63
  • 1
  • 10