Why is a cached ImageSource preventing a page using the ImageSource from being freed?

JohnHardmanJohnHardman GBUniversity mod
edited October 2016 in Xamarin.Forms

As I use certain images repeatedly (e.g. for checkboxes, expanders etc), I added some caching a while back. Caching instances of Image caused a problem as there would be multiple references to the same Image object, but caching ImageSource seemed to be ok (although I haven’t measured yet whether there is any benefit, and I will replace it with the FFImageLoading package soon anyway).

However, I have noticed a memory leak, where pages making use of a cached ImageSource do not get freed up. I may need coffee, but I’m not spotting why this is the case. I would like to understand this before replacing the code with FFImageLoading.

I have a class MyAppToggleImageButton that has a constructor that is passed two ImageSource values. Those ImageSource values are obtained from a cache and used in a page as follows (shortened to show the relevant bits):

public class ExperimentationPageView : MyAppContentPageView
{
    private MyAppToggleImageButton _btnCheck;

    public ExperimentationPageView()
    {
        _btnCheck = new MyAppToggleImageButton(
            MyAppImageCache.TheInstance.UncheckedCheckboxMediumButtonImageSource,
            MyAppImageCache.TheInstance.CheckedCheckboxMediumButtonImageSource,
            initiallyChecked)
        {
        };

        // other stuff here
    }
}

The MyAppToggleImageButton class looks like this (shortened to show the relevant bits):

public class MyAppToggleImageButton : MyAppTemplatedContentView // based on the XLabs templated content view
{
    private Image _toggledImage;
    private Image _untoggledImage;

    // other stuff here

    public MyAppToggleImageButton(
        ImageSource untoggledImageSource,
        ImageSource toggledImageSource,
        bool initiallyToggled)
        : base()
    {
        if (Instance == null)
        {
            Instance = new DataTemplate(
                () =>
                {
                    int imageWidth = _inDepthDebugging ? 22 : 44;

                    _toggledImage = new Image
                    {
                        Aspect = Aspect.AspectFit,
                        Source = toggledImageSource, // leaks resources
                        InputTransparent = true,
                        WidthRequest = imageWidth
                    };

                    _untoggledImage = new Image
                    {
                        Aspect = Aspect.AspectFit,
                        Source = untoggledImageSource, // leaks resources
                        InputTransparent = true,
                        WidthRequest = imageWidth
                    };

                    Binding untoggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppBooleanToDoubleTrueToZeroFalseToOneConverter.Instance
                    };
                    _untoggledImage.SetBinding(
                       Image.OpacityProperty, untoggledBinding); // TODO - change this back

                    Binding toggledBinding = new Binding
                    {
                        Source = this,
                        Path = nameof(MyAppToggleImageButton.IsToggled),
                        Mode = BindingMode.OneWay,
                        Converter = MyAppBooleanToDoubleTrueToOneFalseToZeroConverter.Instance
                    };
                    _toggledImage.SetBinding(
                        Image.OpacityProperty, toggledBinding);

                    _grid = new Grid
                    {
                        BackgroundColor = this.BackgroundColor, // TODO - add binding for this
                        ClassId = "KCI_Grid",
                        ColumnDefinitions = new ColumnDefinitionCollection(),
                        ColumnSpacing = 0,
                        HeightRequest = 44,
                        HorizontalOptions = LayoutOptions.Start,
                        IsClippedToBounds = true,
                        IsVisible = true,
                        MinimumHeightRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumHeightOfTappableObject,
                        MinimumWidthRequest =
                            ViewsUsingXamarinForms.MyAppViewRelatedConstants.MinimumWidthOfTappableObject,
                        Opacity = 1.0,
                        Padding = 0,
                        RowDefinitions = new RowDefinitionCollection(),
                        RowSpacing = 0,
                        VerticalOptions = LayoutOptions.Center,
                        WidthRequest = 44,
                    };

                    _grid.ColumnDefinitions.Add(new ColumnDefinition {Width = imageWidth});
                    _grid.RowDefinitions.Add(new RowDefinition {Height = imageWidth});

                    _grid.Children.Add(_toggledImage, 0, 1, 0, 1);
                    _grid.Children.Add(_untoggledImage, 0, 1, 0, 1);

                    this.HorizontalOptions = LayoutOptions.StartAndExpand;
                    this.VerticalOptions = LayoutOptions.FillAndExpand;
                    this.Padding = 0;
                    return _grid;
                });
        }

        base.ContentTemplate = Instance;

    } // constructor
}

When the page is popped from the navigation stack, the ExperimentationPageView object is not freed up.

The page is only freed if I do:
_toggledImage.Source = null;
_untoggledImage.Source = null;

or, if I simply pass in new ImageSource values rather than cached ImageSource values, the page is freed ok.

So, the question is, why do the pages not get garbage collected if I am using cached ImageSource values, and if I don’t explicitly set the ImageSource properties back to null? Ok, the ImageSource values would have a usage count that won’t go down to zero, but why would that stop the page being freed?

Hoping this is not a question that a mug of coffee would solve… :-)

Answers

  • alexmaniealexmanie ESMember ✭✭

    Same here with latest version. Any fix ?

  • JamesLaveryJamesLavery GBBeta, University ✭✭✭✭✭
    > @alexmanie said:
    > Same here with latest version. Any fix ?

    Do previous versions not exhibit the same behaviour? Are you sure this is occurring oy on the latest version?
  • alexmaniealexmanie ESMember ✭✭

    hi @JamesLavery ,
    totally sure it is occurring on latest version.

    I'm using Prism.Forms to handle navigations, and the navigation doesn't happens when targeting local images on Source property of the image.

Sign In or Register to comment.