はじめに
これまで2回ほどBitmap、BitmapDataを扱った画像処理の記事を記載してきました。
.NETによる画像処理の高速化Tips:非unsafe編
.NETによる画像処理の高速化Tips:unsafe編(改稿:2015/11/08)
これらの中では例外処理について意図的に避けてきましたが、今回は例外処理について記載したいと思います。
例外処理のポイント
Bitmap及びBitmapDataを扱った画像処理を行った場合、以下の二点に気を配る必要があります。
- BitmapはIDisposableを実装している
この為オブジェクトが不要となった時点でもれなくDisposeすること - BitmapDataは不要となった時点で必ずUnlockBitsを呼び出してメモリのロックを解除すること
1.は気が付きやすいのですが、2.は案外例外時の処理が不十分であるコードが散見されます。
Web上のコードサンプルがそこまで記述されていないものが多いため(私の前のコードもですが)、どうしても見落とされがちになってしまうのかも知れません。
また、気を配っているつもりでも意外と忘れやすいケースがあります。
次節ではサンプルを元に解説したいと思います。
例外処理サンプル
まず以下の前提があるとします。
- 何らかの画像処理を行うメソッドを作成する必要がある
- 該当メソッドでは引数にBitmapオブジェクトを受け取り、画像処理結果を引数とは異なるBitmapオブジェクトとして返却する
- 引数として渡されるBitmap及び、戻り値のBitmapのリソース開放はメソッドの呼び出し元が責任を持つ
- 処理の速度を考慮し、内部処理で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点あります。
- dest.LockBitsで例外が発生した場合は、destBitmapDataは確保されていないためアンロックは必要ない
- 「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;
}
ポイントは以下の点です。
- BitmapのLockBits自体が例外をスローする可能性がある
この為、tryブロックを2重で書かなくてはならずはっきり言って気に入りません。
MSDN Bitmap.LockBits メソッド - 画像処理ブロックで例外が発生した場合、戻り値予定であった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;
}
}
}
}