Reputation: 1187
I developed a MAUI application that periodically fetches an image and displays it in an Image control. I used a Timer that ticks every few seconds to grab the latest image, load it into a MemoryStream, and set the Image's Source property to an ImageSource created from that stream via ImageSource.FromStream().
This worked fine initially. However, after running for a period of time, I noticed memory usage climbing steadily even though I was displaying the same image repeatedly.
Upon investigation, I realized the MemoryStream was not being disposed because the ImageSource held a reference to it internally. So each time the Timer ticked, a new MemoryStream was created and the old one was abandoned, causing a memory leak.
Code:
private void OnStartBtnClicked(object sender, EventArgs e)
{
_timer = new Timer(TimerCallback, null, 0, 5000);
StartBtn.IsEnabled = false;
StopBtn.IsEnabled = true;
}
void TimerCallback(object state)
{
try
{
Debug.WriteLine("callback run");
UpdateImage().ConfigureAwait(false);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private async Task ScreenShotAndOcr()
{
// get new image bytes
byte[] imageBytes = ... ...;
await this.Dispatcher.DispatchAsync(async () =>
{
ScreenImage.Source = ImageSource.FromStream(() => new MemoryStream(imageBytes ));
// some other UI update
}
}
To confirm the cause, I commented out the code that set the ImageSource(other code remains the same):
//ScreenImage.Source = ImageSource.FromStream(() => new MemoryStream(imageBytes ));
With this code disabled, the memory usage stayed constant.
I have updated the VS to the latest version(17.7.5), but it didn't work either.
I didn't find any similar issues when searching, so I'm not sure of the best way to handle this problem. Any help would be appreciated.
Upvotes: 1
Views: 1282
Reputation: 1187
After conducting several tests, I believe I may understand the underlying cause
An Image object can hold multiple StreamImageSource objects.
When calling Image.FromStream, the old stream will be disposed.
However, MemoryStream does not release its internal byte array on dispose, which leads to the array not being eligible for garbage collection. One solution is to avoid using MemoryStream, and instead use a stream that clears the byte array on dispose. Another simple approach is to subclass MemoryStream and override Dispose to clear the array.
public class ClearMemoryStream : MemoryStream
{
public ClearMemoryStream(byte[] buffer) : base(buffer)
{
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
FieldInfo field = typeof(MemoryStream).GetField("_buffer", BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null)
{
field.SetValue(this, null);
}
}
}
}
This is a simple workaround, the memory still leak(but very slowly...)
A better approach may be to programmatically generate the Image instead of reusing it.
It has nothing to do with Image, but Dispatcher.DispatchAsync
and FromStream
together.
The closure lambda and async state machine generated from DispatchAsync hold imageBytes and it used in another closure. (DisplayClass5 is the same class as the DisplayClass4, I make some test and it changes the name)
[CompilerGenerated]
private sealed class <>c__DisplayClass5_0
{
public byte[] array;
public MainPage <>4__this;
public Func<Stream> <>9__1;
public <>c__DisplayClass5_0()
{
base..ctor();
}
and
this.<>4__this.<>4__this.ScreenImage.Source = ImageSource.FromStream(this.<>4__this.<>9__1 ?? (this.<>4__this.<>9__1 = new Func<Stream>((object) this.<>4__this, __methodptr(<ScreenShotAndOcr>b__1))));
this.<>4__this is a instance of <>c__DisplayClass5_0.
Func capture this instance and ImageSource keep the Func ... ...
This is really wired.
let's look back the root:
...emmmm It seems the problem is still made by Image itself... ...
a simple fix is to make the imageBytes a field and the closure will disappear.
Upvotes: 1