Search code examples
c#.netsilverlightwindows-phone-7

ImageTools AnimatedImage not releasing memory


I have a WindowsPhone 7.8 application, that needs to display animated GIFs as well as images that are supported directly by Silverlight. Therefor we created a custom user control public sealed partial class ExtendedImageControl : UserControl, IDisposable, which has a DependancyProperty called ImageSource. This property is bound to a URL. The code behind will then either insert a regular Image control into the LayoutRoot, or a AnimatedImage. However, the AnimatedImage will not release its memory when it gets out of view or the containing page is closed.

The loading logic is the following:

ExtendedImage loadedImage = (ExtendedImage)await Utils.Caching.Image.LoadCachedImageFromUrlAsync<ExtendedImage>(loadedLocation);

                        if (ImageSource == loadedLocation)
                        {
                            AnimatedImage image = new AnimatedImage();
                            image.Stretch = stretch;
                            image.Source = loadedImage;
                            LayoutRoot.Children.Add(image);
                            CurrentImageMode = ExtendedImageMode.AnimatedImage;
                            loadedImage = null;
#if DEBUG
                            App.logger.log("Loaded {0} as animated image", loadedLocation);
#endif
                            imageDisplay = image;
                            raiseImageUpdated();
                        }

Ultimately, the image is loaded with

        WebClient client = new WebClient();
        ExtendedImage image = new ExtendedImage();
        using (Stream source = await client.OpenReadTaskAsync(location))
        {

            if (location.ToString().EndsWith("gif", StringComparison.InvariantCultureIgnoreCase))
            {
                image.SetSource(source);

                TaskCompletionSource<ExtendedImage> imageLoaded = new TaskCompletionSource<ExtendedImage>();

                EventHandler loadingCompleteHandler = new EventHandler((sender, e) =>
                {
                    imageLoaded.SetResult(image);
                });

                EventHandler<UnhandledExceptionEventArgs> loadingFailedHandler = new EventHandler<UnhandledExceptionEventArgs>((sender, e) =>
                {
                    imageLoaded.SetResult(image);
#if DEBUG
                    if (System.Diagnostics.Debugger.IsAttached)
                        System.Diagnostics.Debugger.Break();
#endif
                });



                image.LoadingCompleted += loadingCompleteHandler;
                image.LoadingFailed += loadingFailedHandler;

                image = await imageLoaded.Task;

                //Remove handlers, otherwise the object might be kept in the memory
                image.LoadingCompleted -= loadingCompleteHandler;
                image.LoadingFailed -= loadingFailedHandler;
            }
            else
            {
                //... load with native Silverlight methods
            }
        }

        return image;

We noticed the memory problem very early on, so the control implements the IDisposable interface with

    public void Dispose()
    {
        unloadImage();
        GC.SuppressFinalize(this);
    }

    private void unloadImage()
    {
        SmartDispatcher.BeginInvoke(() =>
        {
            if (imageDisplay != null)
            {
                if (imageDisplay is AnimatedImage)
                {
                    if ((imageDisplay as AnimatedImage).Source != null & (imageDisplay as AnimatedImage).Source.Frames != null)
                        (imageDisplay as AnimatedImage).Source.Frames.Clear();

                    (imageDisplay as AnimatedImage).Stop();

                    (imageDisplay as AnimatedImage).Source = null;
                }
                else if (imageDisplay is Image && ((Image)imageDisplay).Source != GIFplaceholder)
                {
                    (imageDisplay as Image).Source = null;
                }

                imageDisplay = null;
            }
        });
    }

However, the Dispose method is never called on the image.

What can I do to find out why this object isn't picked up by the GC? I'm not registering any eventhandlers so as far as I understand, it should be collected when the Application page is navigated away from. I tried adding a destructor as well, however this one is not called as well.


Solution

  • The memory leak is not in ImageTools but is actually a bug in the Silverlight Runtime:

    Memory Leak when you Dynamically add and remove Images

    Workaround: When dynamically adding or removing BitmapImages from an application (a.k.a. adding/removing from the tree), you should set Image.Source = null before removing the Image element from the tree. This will make the BitmapImage eligible for garbage collection. Bug Status: Active bug. *

    References:

    Silverlight: How to unload (dispose) an image from memory? https://blogs.msdn.microsoft.com/silverlight_sdk/2008/10/28/silverlight-bugs-and-workarounds/

    One can quickly verify that setting each ExtendedImage to Nothing before back navigating away from the XAML page can result in dramatic reduction in application memory use.

    I add the following to my RootFrame_Navigating event in App.xaml to help track these leaks:

    Dim applicationCurrentMemoryUsage As Long = Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage")
    Dim applicationPeakMemoryUsage As Long = Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("ApplicationPeakMemoryUsage")
    Debug.WriteLine(DateTime.Now.ToLongTimeString() & " Current : " & applicationCurrentMemoryUsage & "  Peak : " & applicationPeakMemoryUsage)