0

I created an image with this function:

private BitmapImage LoadImage(byte[] imageData)
{
    if (imageData == null || imageData.Length == 0) return null;
    var image = new BitmapImage();
    using (var mem = new MemoryStream(imageData))
    {
        mem.Position = 0;
        image.BeginInit();
        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = null;
        image.StreamSource = mem;
        image.EndInit();
    }
    image.Freeze();
    return image;
}

When I attempt to dispose it:

myImage.StreamSource.Close();
myImage.StreamSource.Dispose();

// Throws an exception since its frozen to read only
//myImage.StreamSource = null;

GC.Collect();

It isn't collected by the garbage collector. Possibly since I can't set it to null.

How can I dispose this BitmapImage so it doesn't live longer in memory?

John
  • 5,942
  • 3
  • 42
  • 79

3 Answers3

0

You've already disposed the StreamSource, since you created the MemoryStream in a using statement, which disposes when leaving the code block. The BitmapImage itself is managed-only and doesn't need to be disposed.

Are you sure it isn't getting cleaned up by the garbage collector? I have a project that makes a lot BitmapImages with BitmapCacheOption.OnLoad and I've never seen a memory leak from it.

(Update) Tested in WPF: (Update2) Needed to add another round of garbage collection. For some reason you have to call it twice for the array to get freed up.

private async void Button_Click(object sender, RoutedEventArgs e)
{
    WeakReference test = this.TestThing();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();

    Debug.WriteLine(test.IsAlive); // Returns false
}

private WeakReference TestThing()
{
    byte[] imageData = File.ReadAllBytes(@"D:\docs\SpaceXLaunch_Shortt_3528.jpg");

    var image = new BitmapImage();
    using (var mem = new MemoryStream(imageData))
    {
        image.BeginInit();
        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = null;
        image.StreamSource = mem;
        image.EndInit();
    }

    image.Freeze();

    return new WeakReference(image);
}
RandomEngy
  • 14,931
  • 5
  • 70
  • 113
  • 1
    I can confirm with this code that the byte array isn't released, there is still a reference somewhere. You can test it yourself by adding a `WeakReference` to it, and checking the `IsAlive` property. I am doing this in release mode with no debugger attached. – Stuart Jan 22 '17 at 23:48
  • Added some test code. I'm not sure why `IsAlive` is `true` for you in that case. If I have the GC call in the same function and Debug mode it returns `true`, but if it's in the same function and in Release mode it's still `false`. maybe you could share the exact code you used? – RandomEngy Jan 23 '17 at 03:47
  • @Stuart I also get the same results when tracking the byte array that was passed into the memory stream. – RandomEngy Jan 23 '17 at 06:37
  • 1
    @Stuart my bad, I needed to add another round of garbage collection before it actually works. – RandomEngy Jan 24 '17 at 06:02
0

We can investigate the issue with the following code:

public static void Main()
{
    var readAllBytes = File.ReadAllBytes(@"SomeBitmap.bmp");
    var wr = new WeakReference(readAllBytes);
    var result = LoadImage(readAllBytes);
    readAllBytes = null;

    //result.StreamSource = null;

    result = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine($"IsAlive: {wr.IsAlive}");
    Console.ReadLine();
}

private static BitmapImage LoadImage(byte[] imageData)
{
    if (imageData == null || imageData.Length == 0) return null;
    var image = new BitmapImage();
    using (var mem = new MemoryStream(imageData))
    {
        mem.Position = 0;
        image.BeginInit();
        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = null;
        image.StreamSource = mem;
        image.EndInit();
    }
    image.Freeze();
    return image;
}

I have tried many variations of caches settings, I cannot find a way to release the byte array, it looks like a bug in WPF.

As you can see, after 2 GC collections the byte array is released.

Edit 1: Simplifying BitmapImage by removing Freeze and the Init methods do release the byte array:

private static BitmapImage LoadImage(byte[] imageData)
{
    if (imageData == null || imageData.Length == 0) return null;
    var image = new BitmapImage();
    using (var mem = new MemoryStream(imageData))
    {
        mem.Position = 0;
        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.UriSource = null;
        image.StreamSource = mem;
    }
    return image;
}

Edit 2: I as noted by others, the byte array is released after 2 rounds of garbage collection, I've update the top example. I found this really useful, and it goes to show that the framework isn't a black box.

Stuart
  • 5,358
  • 19
  • 28
  • The strange thing is, that if I do it in bulk. It also doesn't release unless I do another bulk of images. – John Jan 23 '17 at 14:27
  • I removed the freeze and now the garbage collection happens when I create a new image and EndInit() is called. – John Jan 23 '17 at 14:55
  • Thanks @John, I've verified your findings and have updated my answer to include it =) – Stuart Jan 23 '17 at 15:01
  • Creating a whole new image to force the garbage collector isn't an ideal solution though. Need to figure out what is invoking the collection that wasn't happening before. @Stuart – John Jan 23 '17 at 15:03
  • Ahh I see what the difference is. When testing I just happened to have GC.Collect() called in two places. If you call it once, it doesn't free it up, but the next round gets it. I assume that's why I don't see a memory leak in my app; the memory doesn't get cleaned up immediately but the next one that comes around gets it. – RandomEngy Jan 24 '17 at 06:00
  • Thanks @RandomEngy, you hit the nail on head! There must be a finalizer involved somewhere. This was a useful exercise – Stuart Jan 24 '17 at 11:59
0

In my case, I have need to switch between a bunch of large floor plans stored as byte arrays on a server. Once all of these are loaded into memory and after some switching between them, I was sitting at 3GB and started to get issues with anything being loaded at all. The images themselves range from 600KB to a few megabytes. When they are drawn, they take up a lot more room.

I found a wrapping stream implementation here (http://faithlife.codes/blog/2009/05/wrappingstream_implementation/) by Bradley Grainger.

This keeps my memory usage reasonably low; less than 2GB most of the time.

WrapperStream MasterStream{get;set;}

private void ChangeImage()
{
            SelectedImage = null;
            GC.Collect();
            var stream = new MemoryStream(ImageSource);

            using (MasterStream = new WrappingStream(stream))
            {
                var bitmap = new BitmapImage();
                bitmap.BeginInit();
                bitmap.StreamSource = MasterStream;
                bitmap.CacheOption = BitmapCacheOption.OnLoad;
                bitmap.EndInit();
                bitmap.Freeze();
                SelectedImage = bitmap;
            }

            GC.Collect();

 }