Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

CreateBitmapSourceFromHBitmapでメモリリークが発生した件

先日よりC#で画面スクリーンショット方法をまとめていました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com


そこで、CreateBitmapSourceFromHBitmap関数でどうもメモリリークが発生してしまい、
メモリ使用量がどんどん増えていく。。

ということで原因と対応策を調べたので備忘録としてまとめておくことしました。
※同じミスはしたくないので。



■原因

メモリリークが発生していたコードですが、こちらのような実装をしておりました。

using (var bmtpScreen = new System.Drawing.Bitmap(
    (int)SystemParameters.PrimaryScreenWidth,
    (int)SystemParameters.PrimaryScreenHeight,
    System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
    using (var bmpFromImg = Graphics.FromImage(bmtpScreen))
    {
        bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size);
        
        Imaging.CreateBitmapSourceFromHBitmap(
            bmtpScreen.GetHbitmap(),
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
    }
}

こちらのコードですと、
CreateBitmapSourceFromHBitmap実行を繰り返す度にメモリリークが発生してしまい、
困ったことになりました。

そもそもCreateBitmapSourceFromHBitmap関数の引数ですが、
 ・第1引数:アンマネージ ビットマップへのポインター(IntPtr)
 ・第2引数:ビットマップのパレットマップへのポインター(IntPtr)
 ・第3引数:ソースイメージのサイズ(Int32Rect)
 ・第4引数:変換を処理する方法を指定する列挙体の値(BitmapSizeOptions)
となります。
※参考:https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.interop.imaging.createbitmapsourcefromhbitmap?view=windowsdesktop-6.0

自分で作成したコードですが、

return Imaging.CreateBitmapSourceFromHBitmap(
                        bmtpScreen.GetHbitmap(),
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromEmptyOptions());

でしたので、第1引数のbmtpScreen.GetHbitmap()が怪しそう。。

ということで調べてみたところ、
第1引数に与えている bmtpScreen.GetHbitmap()は明示的に開放しないとメモリリークにつながることを発見。
fkmt5.hatenadiary.org

自分のコードだとメモリ開放はしていないので、
メモリが開放されず蓄積してしまいメモリリークにつながってしまっていたようです。

■解決方法

bmtpScreen.GetHbitmap()のメモリを解放すればよいことが分かりましたので、実際に開放処理を追加すればOK。
開放にはDeleteObject()を利用することにしました。

DeleteObject()とは、、、
WIn32 APIの1つで、
論理ペン、ブラシ、フォント、ビットマップ、領域、またはパレットを削除し、オブジェクトに関連付けられているすべてのシステムリソースを解放
する関数になります。
オブジェクトが削除されると、指定されたハンドルは無効になります。
※参考:https://docs.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-deleteobject

DeleteObject()を利用する場合にはあらかじめ、

        [System.Runtime.InteropServices.DllImport("gdi32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);

と定義すればOK。

後は、

using (var bmtpScreen = new System.Drawing.Bitmap(
    (int)SystemParameters.PrimaryScreenWidth,
    (int)SystemParameters.PrimaryScreenHeight,
    System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
    using (var bmpFromImg = Graphics.FromImage(bmtpScreen))
    {
        bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size);
        
        var ptrBuf = bmtpScreen.GetHbitmap();

        Imaging.CreateBitmapSourceFromHBitmap(
            ptrBuf ,
            IntPtr.Zero,
            Int32Rect.Empty,
            BitmapSizeOptions.FromEmptyOptions());
      
            DeleteObject(ptrBuf);
    }
}

といったように、
DeleteObjectで、

var ptrBuf = bmtpScreen.GetHbitmap()

で定義したリソースを明示的に開放してあげればOKです。

■最後に

今回は自分が躓いたメモリリークに関してまとめておきました。
次回同じようなコードを書く時には気を付けたいと思います。