題記の通りなのですが、一時ハマったので共有します。 正確には、「バックグラウンド スレッドで DispatcherObject を作るとメモリリークする」ですね。
例えば、バックグラウンドで画像をダウンロードして加工したり。 もしくは Grid や TextBlock といった UI 要素を使って、サムネイルやら何やらの画像を生成したいときとか。
ImageSource は Freezable なので、バックグラウンドで画像を作って Freeze() してしまえば、UI スレッドに渡してもだいじょうぶ。 なるべく UI スレッドの負担を減らしたいのです。
ということで、以下のようなコードを書いてみます。 サンプルなので Console アプリですが、PresentationCore, PresentationFramework, WindowsBase, System.Xaml あたりを参照すればよろし。
static void Work() { var border = new Border { Background = Brushes.Red }; var text = new TextBlock { Text = "hoge" }; border.Child = text; border.Arrange(new Rect(0, 0, 200, 200)); var bitmap = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Default); bitmap.Render(border); bitmap.Freeze(); // 生成したビットマップ使って何かする }
で、このビットマップを作るコードを、バックグラウンドスレッドで動かします。 試しに 10 回ほど。
static void Main(string[] args) { for (var i = 0; i < 10; i++) { var thread = new Thread(Work) { Name = "Bitmap worker: " + i, IsBackground = true, }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); Console.WriteLine("Total Memory = {0} KB", GC.GetTotalMemory(true) / 1024); } Console.ReadLine(); }
ちなみに、バックグラウンドでやろうとすると「多数の UI コンポーネントが必要としているため、STA である必要があります。」といった例外がスローされるようなもの (XpsDocument とか) でも、Thread.SetApartmentState メソッドでスレッドを STA にしてやれば、ちゃんと動きます。
これを実行すると…
メモリ使用量がどんどん増えていきます。 これではダメですね。
そこで、最初のコードに以下の 2 行を追加します。
static void Work() { var border = new Border { Background = Brushes.Red }; var text = new TextBlock { Text = "hoge" }; border.Child = text; border.Arrange(new Rect(0, 0, 200, 200)); var bitmap = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Default); bitmap.Render(border); bitmap.Freeze(); // 生成したビットマップ使って何かする Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle); Dispatcher.Run(); }
ご覧の通り、Dispatcher をシャットダウンするコードです。 これを実行すると…
いい感じです。
さて、この原因についてですが。
DispatcherObject を生成した時点で、スレッドごとに新たな Dispatcher が生成されるため、…と考えられます。 (Dispatcher のソースコードを軽く読んだところ、スレッドごとに生成された Dispatcher は WeakReference で保持されてるようですが、果たして…?)
ご参考まで。 詳しい事情が判ったらまた展開します。
2014/05/20 追記:
@karno 氏が詳しい調査をしてくださいました。
Krile STARRYEYES に入った修正も参考にしつつ。メイン スレッド外で DispatcherObject を生成する場合は、かなり気を遣いましょう。
WPF に関する型ってだいたい DispatcherObject の派生型なので、気付かず使ってるパターンもありそうです。Dispatcher のシャットダウンまで面倒見てくれるユーティリティ等を用意したほうがよさげ、かな。