c# - WPF CreateBitmapSourceFromHBitmap() pérdida de memoria
.net memory-leaks (4)
Necesito dibujar una imagen píxel por píxel y mostrarla dentro de un WPF. Estoy intentando hacer esto usando un System.Drawing.Bitmap
luego usando CreateBitmapSourceFromHBitmap()
para crear un BitmapSource
para un control de imagen WPF. Tengo una pérdida de memoria en alguna parte porque cuando se llama repetidamente al CreateBitmapSourceFromBitmap()
el uso de la memoria aumenta y no baja hasta que la aplicación finaliza. Si no llamo CreateBitmapSourceFromBitmap()
no hay cambios notables en el uso de la memoria.
for (int i = 0; i < 100; i++)
{
var bmp = new System.Drawing.Bitmap(1000, 1000);
var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
bmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty,
System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
source = null;
bmp.Dispose();
bmp = null;
}
¿Qué puedo hacer para liberar la memoria de BitmapSource
?
Esta es una gran (!!) publicación, aunque con todos los comentarios y sugerencias, me llevó una hora descubrir las piezas. Así que aquí hay una llamada para obtener BitMapSource con SafeHandles, y luego un uso de ejemplo para crear un archivo de imagen .PNG. En la parte inferior están los ''usos'' y algunas de las referencias. Por supuesto, ninguno de los méritos es mío, solo soy el escriba.
private static BitmapSource CopyScreen()
{
var left = Screen.AllScreens.Min(screen => screen.Bounds.X);
var top = Screen.AllScreens.Min(screen => screen.Bounds.Y);
var right = Screen.AllScreens.Max(screen => screen.Bounds.X + screen.Bounds.Width);
var bottom = Screen.AllScreens.Max(screen => screen.Bounds.Y + screen.Bounds.Height);
var width = right - left;
var height = bottom - top;
using (var screenBmp = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
BitmapSource bms = null;
using (var bmpGraphics = Graphics.FromImage(screenBmp))
{
IntPtr hBitmap = new IntPtr();
var handleBitmap = new SafeHBitmapHandle(hBitmap, true);
try
{
bmpGraphics.CopyFromScreen(left, top, 0, 0, new System.Drawing.Size(width, height));
hBitmap = screenBmp.GetHbitmap();
using (handleBitmap)
{
bms = Imaging.CreateBitmapSourceFromHBitmap(
hBitmap,
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
} // using
return bms;
}
catch (Exception ex)
{
throw new ApplicationException($"Cannot CopyFromScreen. Err={ex}");
}
} // using bmpGraphics
} // using screen bitmap
} // method CopyScreen
Aquí está el uso, y también la clase "Manija segura":
private void buttonTestScreenCapture_Click(object sender, EventArgs e)
{
try
{
BitmapSource bms = CopyScreen();
BitmapFrame bmf = BitmapFrame.Create(bms);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(bmf);
string filepath = @"e:/(test)/test.png";
using (Stream stm = File.Create(filepath))
{
encoder.Save(stm);
}
}
catch (Exception ex)
{
MessageBox.Show($"Err={ex}");
}
}
public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern int DeleteObject(IntPtr hObject);
[SecurityCritical]
public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
: base(ownsHandle)
{
SetHandle(preexistingHandle);
}
protected override bool ReleaseHandle()
{
return DeleteObject(handle) > 0;
}
}
Y finalmente un vistazo a mis ''usos'':
using System;
using System.Linq;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using System.Windows.Interop;
using System.Windows;
using System.IO;
using Microsoft.Win32.SafeHandles;
using System.Security;
Los archivos DLL a los que se hace referencia incluyen: * PresentationCore * System.Core * System.Deployment * System.Drawing * WindowsBase
MSDN afirma que para Bitmap.GetHbitmap()
: usted es responsable de llamar al método GDI DeleteObject para liberar la memoria utilizada por el objeto GDI bitmap. Entonces usa el siguiente código:
// at class level
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
// your code
using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(1000, 1000))
{
IntPtr hBitmap = bmp.GetHbitmap();
try
{
var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
}
finally
{
DeleteObject(hBitmap);
}
}
También reemplacé su llamada a Dispose()
por una instrucción de using
.
Siempre que se maneje con identificadores no administrados, puede ser una buena idea usar los contenedores de "manejo seguro":
public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid
{
[SecurityCritical]
public SafeHBitmapHandle(IntPtr preexistingHandle, bool ownsHandle)
: base(ownsHandle)
{
SetHandle(preexistingHandle);
}
protected override bool ReleaseHandle()
{
return GdiNative.DeleteObject(handle) > 0;
}
}
Construye uno así tan pronto como muestres un identificador (idealmente tus API nunca expondrían IntPtr, siempre devolverían identificadores seguros):
IntPtr hbitmap = bitmap.GetHbitmap();
var handle = new SafeHBitmapHandle(hbitmap , true);
Y úsalo así:
using (handle)
{
... Imaging.CreateBitmapSourceFromHBitmap(handle.DangerousGetHandle(), ...)
}
La base SafeHandle le brinda un patrón automático de desechables / finalizador, todo lo que necesita hacer es anular el método ReleaseHandle.
Tenía el mismo requisito y problema (pérdida de memoria). Implementé la misma solución marcada como respuesta. Pero aunque la solución funciona, causó un golpe inaceptable para el rendimiento. Corriendo en un i7, mi aplicación de prueba vio una constante de 30-40% de CPU, 200-400 MB de RAM aumenta y el recolector de basura funcionaba casi cada milisegundo.
Como estoy procesando video, necesito un rendimiento mucho mejor. Se me ocurrió lo siguiente, así que pensé que lo compartiría.
Objetos globales reutilizables
//set up your Bitmap and WritableBitmap as you see fit
Bitmap colorBitmap = new Bitmap(..);
WriteableBitmap colorWB = new WriteableBitmap(..);
//choose appropriate bytes as per your pixel format, I''ll cheat here an just pick 4
int bytesPerPixel = 4;
//rectangles will be used to identify what bits change
Rectangle colorBitmapRectangle = new Rectangle(0, 0, colorBitmap.Width, colorBitmap.Height);
Int32Rect colorBitmapInt32Rect = new Int32Rect(0, 0, colorWB.PixelWidth, colorWB.PixelHeight);
Código de conversión
private void ConvertBitmapToWritableBitmap()
{
BitmapData data = colorBitmap.LockBits(colorBitmapRectangle, ImageLockMode.WriteOnly, colorBitmap.PixelFormat);
colorWB.WritePixels(colorBitmapInt32Rect, data.Scan0, data.Width * data.Height * bytesPerPixel, data.Stride);
colorBitmap.UnlockBits(data);
}
Ejemplo de implementación
//do stuff to your bitmap
ConvertBitmapToWritableBitmap();
Image.Source = colorWB;
El resultado es una CPU estable de 10-13%, RAM de 70-150 MB, y el recolector de basura solo se ejecutó dos veces en una ejecución de 6 minutos.