30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

.NETでBitmap、BitmapDataを扱う場合の例外処理について

Last updated at Posted at 2015-11-08

はじめに

これまで2回ほどBitmap、BitmapDataを扱った画像処理の記事を記載してきました。
.NETによる画像処理の高速化Tips:非unsafe編
.NETによる画像処理の高速化Tips:unsafe編(改稿:2015/11/08)

これらの中では例外処理について意図的に避けてきましたが、今回は例外処理について記載したいと思います。

例外処理のポイント

Bitmap及びBitmapDataを扱った画像処理を行った場合、以下の二点に気を配る必要があります。

  1. BitmapはIDisposableを実装している
    この為オブジェクトが不要となった時点でもれなくDisposeすること
  2. BitmapDataは不要となった時点で必ずUnlockBitsを呼び出してメモリのロックを解除すること

1.は気が付きやすいのですが、2.は案外例外時の処理が不十分であるコードが散見されます。
Web上のコードサンプルがそこまで記述されていないものが多いため(私の前のコードもですが)、どうしても見落とされがちになってしまうのかも知れません。

また、気を配っているつもりでも意外と忘れやすいケースがあります。
次節ではサンプルを元に解説したいと思います。

例外処理サンプル

まず以下の前提があるとします。

  1. 何らかの画像処理を行うメソッドを作成する必要がある
  2. 該当メソッドでは引数にBitmapオブジェクトを受け取り、画像処理結果を引数とは異なるBitmapオブジェクトとして返却する
  3. 引数として渡されるBitmap及び、戻り値のBitmapのリソース開放はメソッドの呼び出し元が責任を持つ
  4. 処理の速度を考慮し、内部処理でBitmapDataを利用する

よくありがちな例だと思います。
具体的なコード例は以下の通りです。

public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    // ビットマップをロックする
    var destBitmapData =
        dest.LockBits(
            new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
            System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
    // 何らかの画像処理を実施する
    ...

    // 処理が完了したためBitmapDataをアンロックする
    dest.UnlockBits(destBitmapData);
    return dest;
}

さて上のコードは鋭い方はすぐ気が付くかも知れませんが、画像処理ブロックで例外が発生した場合、BitmapDataがアンロックされず、メモリがリークする原因となってしまいます。
というわけで、例外が発生してもきちんとアンロックされるように修正します。
以下の通りです。

public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    // ビットマップをロックする
    var destBitmapData =
        dest.LockBits(
            new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
            System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
    try
    {
        // 何らかの画像処理を実施する
        ...
    }
    finally
    {
        // 処理が完了したためBitmapDataをアンロックする
        dest.UnlockBits(destBitmapData);
    }
    return dest;
}

ポイントは2点あります。

  1. dest.LockBitsで例外が発生した場合は、destBitmapDataは確保されていないためアンロックは必要ない
  2. 「tryブロックに入っている」=「destBitmapData」は必ず存在するためfinally句でnullチェックは必要ない

さて、一見上で問題ないように見える。。。というか、私は見えてやらかしたんですが、以下の問題点があります。
「画像処理ブロックで例外が発生した場合、destのBitmapのDisposeが呼び出されない」

きっと見落としがちなはず!(私だけじゃないはず!)
戻り値のBitmapは、ちゃんとreturnできた場合は、呼び出し元でリソースの開放責任を持つため、失念しがちです。(きっと!)

そんなわけで、その点も考慮したコードが以下になります。

public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    try
    {
        // ビットマップをロックする
        var destBitmapData =
            dest.LockBits(
                new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
                System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
        try
        {
            // 何らかの画像処理を実施する
            ...
        }
        finally
        {
            // 処理が完了したためBitmapDataをアンロックする
            dest.UnlockBits(destBitmapData);
        }
    }
    catch
    {
        dest.Dispose();
        throw;
    }
    return dest;
}

ポイントは以下の点です。

  1. BitmapのLockBits自体が例外をスローする可能性がある
    この為、tryブロックを2重で書かなくてはならずはっきり言って気に入りません。
    MSDN Bitmap.LockBits メソッド
  2. 画像処理ブロックで例外が発生した場合、戻り値予定であったBitmapオブジェクトのdestは返却されないため、catchブロックでDisposeし、例外は再スローする

上のコードも正直汚くてあまり気に入りませんが、実際の場合、destのBitmapだけではなくsrcのBitmapもロックしてBitmapDataを利用するケースが多いはずです。
こうなると、try・catch・finallyを何重にも記載しなくてはならず、コードの可読性は下がる上に、リソースの開放漏れも気が付きにくくなります。

これらを回避するためには、IDisposableを実装した、BitmapDataのラッパークラスを作成するのが良いかもしれません。(ラッパークラスのサンプルは末尾に記載します。)
すると以下のように比較的きれいに記述できますし、例外処理の漏れも防げます。

public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);

    try
    {
        using (var srcBitmapData = BitmapDataWrapper.LockBits(src))
        using (var destBitmapData = BitmapDataWrapper.LockBits(dest))
        {
            // 何らかの画像処理を実施する
        }
    }
    catch
    {
        dest.Dispose();
        throw;
    }

    return dest;
}

IDisposableを実装したBitmapDataのサンプルは以下の通りです。

/// <summary>
/// BitmapDataをラップしたヘルパークラス
/// </summary>
public class BitmapDataWrapper : IDisposable
{
    /// <summary>
    /// ロック対象となったBitmap
    /// </summary>
    private System.Drawing.Bitmap bitmap;
    /// <summary>
    /// Bitmapをロックしたオブジェクト
    /// </summary>
    private System.Drawing.Imaging.BitmapData bitmapData;
    /// <summary>
    /// Bitmapをロックしたオブジェクトを取得する
    /// </summary>
    public System.Drawing.Imaging.BitmapData BitmapData
    {
        get { return bitmapData; }
    }
    /// <summary>
    /// コンストラクタは隠蔽し、LockBitsメソッドを利用する
    /// </summary>
    /// <param name="bitmap">ロック対象となったBitmap</param>
    /// <param name="bitmapData">Bitmapをロックしたオブジェクト</param>
    private BitmapDataWrapper(System.Drawing.Bitmap bitmap, System.Drawing.Imaging.BitmapData bitmapData)
    {
        this.bitmap = bitmap;
        this.bitmapData = bitmapData;
    }
    /// <summary>
    /// デストラクタ
    /// </summary>
    ~BitmapDataWrapper()
    {
        Dispose(false);
    }
    /// <summary>
    /// Bitmapをシステムメモリにロックします。
    /// </summary>
    /// <param name="bitmap"></param>
    /// <returns></returns>
    public static BitmapDataWrapper LockBits(System.Drawing.Bitmap bitmap)
    {
        var bitmapData =
            bitmap.LockBits(
                    new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
        return new BitmapDataWrapper(bitmap, bitmapData);
    }
    /// <summary>
    /// リソースを開放する
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    /// <summary>
    /// リソースを開放する
    /// </summary>
    /// <param name="disposing"></param>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (bitmap != null)
            {
                bitmap.UnlockBits(bitmapData);
                bitmap = null;
                bitmapData = null;
            }
        }
    }
}
30
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?