Windowsフォームアプリケーションでリソースリークしないために(2)
連載記事にするつもりが、第1回のみ公開して第2回は下書きに放置していた。第2回が書きかけだからだが、当時のことをほぼ完全に忘れているので書き切れそうにない。なのでエイヤッと公開しておく。このあとControl.InvokeやBeginInvokeに関する話(この辺とか)を書きたかったような気がするのだが…。
画像非同期読み込みコントロールについて
今回特に多くリソースリークしたのが画像非同期読み込みコントロールだ。本エントリでも何度も触れる(はずな)ので、その概要を説明しておく。
当該コントロール(以降「AsyncPictureBox」とする)はPictureBoxをラップしたユーザーコントロールだ *1。DBにBLOB値として格納されている画像を読み込むpublicメソッド(以降「BeginLoadメソッド」とする)を公開している。画像をDBから読み込むので当然、パフォーマンス上の懸念がある。そこで画像読み込みは、非メインスレッドで非同期に行う。これにより画像の読み込みが完了しなくても操作が可能になるので、ユーザーにとっての体感速度が向上する。
画像を表示する枠が表示されているのに画像の非同期読み込みが完了するまでそこに何も表示されないのは、何だか寂しい。よってその間、読み込み中だと分かるGIFアニメーション画像(こういうの)を表示する。
DBから画像を読み込もうとしたら、存在しないことがあり得る。DB内の画像はいつ削除されるか分からない(そういうシステムだ)ためだ。AsyncPictureBoxはそういった場合、画像が存在しないことを表す画像(以降「NoImage画像」とする)を表示する。その画像は静的なので、アプリケーションにリソースとして埋め込む。
最後にAsyncPictureBoxは、場合によっては同時に500個程度も配置される。
動的コントロール破棄時は、Remove/Clearの後にDispose
Visual Studioのデザイナだけでコントロールの配置が事足りることはあまりない。大抵の場合データの状態などに応じて配置すべきコントロールの数などが変化するため、デザイナでは事足りない。そのような場合一般的に、コントロールのインスタンスを生成するコードを自分で書くが、そうやって生成したコントロールのことを動的コントロールと言う。
動的コントロールのライフサイクル*2はしばしば、その親コントロールのライフサイクルより短い。よくあるのが例えば、PanelがFormと同じライフサイクルで、そのPanel内に動的コントロールを配置、あるボタンが押されるとPanel内のコントロールを全てクリアし、またそのPanel内に動的コントロールを配置、といった感じだ。
Windowsフォームアプリケーションプログラミングの基礎で、コントロールの破棄時にはDisposeしなければならない。*3そこで次のようなユーティリティメソッド*4が書ける。
public static void ClearControls(this Control control) { if (control == null) { throw new ArgumentNullException("control"); } var controls = control.Controls.Cast<Control>().ToArray(); control.Controls.Clear(); foreach (var ctrl in controls) { ctrl.Dispose(); } }
ここでポイントなのが、DisposeをClearより後に行っている点だ。そのためにClearの前にControlsをバックアップしておくという、少々直感的でないコードになっている。なぜそうするかというと、Dispose済みのオブジェクトへの参照がないようにする、という.NETプログラミングの鉄則に則るためだ。1つのメソッド内の話なので、マルチスレッド絡みでもない限り問題が起きそうにはないが。
と、ここまで書いて説明の順番を誤ったと気づいた。この場合はDispose→Clearでも問題が起きたことがないのだった。もしかするとウィンドウハンドルがリークするのではないかと思うが、そこまでの確認をしていない。ただこの順序が非常に重要なケースがある。それを次に説明する。
ImageListをたくさん配置してはいけない
AsyncPictureBoxの初期の実装では、PictureBox以外にもう1つ、ImageListを配置していた。そしてVisual Studioのデザイナで、そのImageListにNoImage画像を格納しておいた。その結果、あまりに多くメモリを消費することになってしまった。まずはその理由を説明する。
Visual StudioのデザイナでImageListを配置して画像を読み込ませると、画像のバイナリデータが文字列化*5されてRESXファイルに保存される。アプリケーションの実行時には、当該ImageListを配置したコントロールのコンストラクタ内で、RESXの文字列から実際の画像のバイナリデータに変換される。
従ってAsyncPictureBoxのコンストラクタ内では、実際にNoImage画像が表示されるかどうかによらずNoImage画像のバイナリデータがメモリ内に配置されることになる。これはあまりにメモリの無駄だ。
ではどうするかというと、アセンブリに埋め込んだ画像リソースを使う。なぜ画像リソースにすべきなのか。理由は簡単で、画像リソースなら必要になった時にだけメモリ内に配置することができる(自然にそうなる)からだ。
Visual Studioを使えば画像リソースをアセンブリに埋め込むのは直感的で簡単だ。DOBON.NETさんが素晴らしい記事を書いてくれている。