[WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について
この記事は、前回のエントリ WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
の続きになります。
目次
前回は、Web画像のURLからBitmapImageを、UIをブロックする事無く生成するには、以下の様に記述すると良さそう。 しかし、このコードにはメモリリークを引き起こす問題が2つあります。 上のコードでは、EndInit()での初期化が終わった時点でStreamSourceプロパティに設定したMemoryStreamは不要になり、using のスコープを抜けるとDispose()されます。 しかし、MemoryStream では、コンストラクタに渡した byte配列の参照を内部バッファとして保持しているのですが、それをDispose()後も抱えたまま離さないようなのです。
※ Dispose()後も_buffer が残ったまま! 画像データとしては、Bitmapにデコードされた表示用の画素データと、デコード前のbyte配列(内部バッファ)の両方を持つことになります。 対処法は、以下サイトにありました。 MemoryStreamをラップしたStreamクラスを作って Dispose時にMemoryStreamの参照を外すという方法でこの問題を回避するようです。 以下のようなStreamのラップクラスを用意して MemoryStream をこれでラップしたうえで StreamSourceプロパティに設定します。 以下、サムネイル画像50枚分のBitmapImageを作成した状態のスナップショットでヒープを比較したものです。効果は一目瞭然だと思います。 ・MemoryStreamのみの場合
・WrappingStreamを使用した場合
これで問題の1つは解決できました。 そして、もう1つの問題が以下になります。 この問題については、以下のエントリに詳しいので、そちらをご参照ください。 上記エントリで記述されている通り、 最も時間のかかるダウンロード部分のみバックグラウンドで非同期実行して、ダウンロードした結果を基にBitmapImageを生成・初期化する部分はUIスレッド上で行う様にします。 まずは、画像データのダウンロード部分。 (2015/10/26 訂正) 上記メソッドで取得したbyte配列を受け取って、MemoryStream を作ります。 上の2つのメソッドを組み合わせて呼び出します。 Rxを使って一度にダウンロードする場合は、前回のおさらい
というところまで書きました。public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new System.Net.Http.HttpClient())
{
//HttpClientでbyte配列に先読みしておいて
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
// そのbyte配列でMemoryStream を作成し、StreamSourceプロパティに設定
using (var stream = new MemoryStream(bytes))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
まずは、そのうちの1つからMemoryStream はDisposeしても内部バッファを離さない
この状態のBitmapImageは、単純計算で必要な量の2倍のメモリ空間を占有しているってことです。
特に大きな画像を読み込んだ場合は、なかなか無視できない容量になるのではないでしょうか。Dispose時にMemoryStreamの参照を外すストリームクラスでラップする
/// <summary>
/// ストリームのWrapperクラス
/// </summary>
/// <remarks>
/// Dispose 時に、内部ストリームの参照を外します
/// </remarks>
public class WrappingStream : Stream
{
Stream m_streamBase;
public WrappingStream(Stream streamBase)
{
if (streamBase == null)
{
throw new ArgumentNullException("streamBase");
}
m_streamBase = streamBase; //渡したStreamを内部ストリームとして保持
}
//Streamクラスのメソッドをオーバーライドして、内部ストリームの同じメソッドをそのまま呼ぶだけ
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
{
ThrowIfDisposed();
return m_streamBase.ReadAsync(buffer, offset, count, cancellationToken);
}
public new Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
return m_streamBase.ReadAsync(buffer, offset, count);
}
//...(中略)...
protected override void Dispose(bool disposing)
{
if (disposing)
{
m_streamBase.Dispose();
m_streamBase = null; //disposeしたら内部ストリームをnullにして参照を外す
}
base.Dispose(disposing);
}
private void ThrowIfDisposed()
{
if (m_streamBase == null)
{
throw new ObjectDisposedException(GetType().Name);
}
}
}
public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new HttpClient())
{
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
using (var stream = new WrappingStream(new MemoryStream(bytes)))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
DispatcherObjectをバックグラウンドスレッドで作成するとメモリリークする
BeginInvokeShutdown()
と Dispatcher.Run
を使って回避する方法もあるようなのですが、
今回はバックグラウンドスレッド上でBItmapImageを生成しないようにすることで、対処したいと思います。ダウンロード処理とBitmapImage生成処理を分離する
画像データを非同期ダウンロードしてbyte配列で返す
System.Net.Http.HttpClient
の GetByteArrayAsync() の結果をそのまま返すだけです。
System.Net.Http.HttpClient
の GetByteArrayAsync() を await してその結果を返します。// このままではDownload完了前に HttpClient がDispose()されてしまう
//public static Task<byte[]> DownLoadImageBytesAsync(string url)
//{
// using (var web = new HttpClient())
// {
// return web.GetByteArrayAsync(url);
// }
//}
public static async Task<byte[]> DownLoadImageBytesAsync(string url)
{
using (var web = new HttpClient())
{
return await web.GetByteArrayAsync(url);
}
}
受け取ったbyte配列からBitmapImageを作成する
さらに、WrappingStreamにラップしたものをStreamSourceプロパティに設定してBitmapImageを初期化します。public static BitmapImage CreateBitmap(byte[] bytes, bool freezing = true)
{
using (var stream = new WrappingStream(new MemoryStream(bytes)))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
if (freezing && bitmap.CanFreeze)
{ bitmap.Freeze(); }
return bitmap;
}
}
DownLoadImageBytesAsync()をバックグラウンドで実行後、指定したディスパッチャ上でCreateBitmap()を実行する
CreateBitmap() を実行するDispatcher を引数で指定できるようにしています。
今回はUIスレッド上で処理を行いたいので、App.Current.Dispatcher
を指定します。public static async Task<BitmapImage> DownloadImageAsync(string url,
Dispatcher dispatcherForBitmapCreation)
{
var bytes = await DownLoadImageBytesAsync(url).ConfigureAwait(false);
return await dispatcherForBitmapCreation.InvokeAsync(() => CreateBitmap(bytes));
}
// 使用例
// var bitmap = await DownloadImageAsync(downloadUrl, App.Current.Dispatcher);
おまけ
DownloadImageAsync()
にまとめず、個別に実行する方が分かりやすいかもしれません。private ObservableCollection imageSource;
public void DownloadSearchResult(IEnumerable<Uri> imageUrlList)
{
//IEnumerable<T>を、スレッドプール上で動作するIObservable<T> に変換
disposable = imageUrlList.ToObservable(ThreadPoolScheduler.Instance)
//画像データをまとめてダウンロード
.SelectMany(async uri => await DownLoadImageBytesAsync(uri))
//UIスレッドに処理を移す
.ObserveOn(Reactive.Bindings.UIDispatcherScheduler.Default)
//BitmapImageを作成してコレクションに追加
.Subscribe( bytes => imagesSource.Add(CreateBitmap(bytes));
}
//ここで使用しているReactive.Bindings.UIDispatcherScheduler は
//ReactiveProperty が用意するクラスです。App.xaml.cs でAppクラスのOnStartup()あたりで
// Reactive.Bindings.UIDispatcherScheduler.Initialize();
//としておくと、UIDispatcherScheduler.Default で UIスレッドのDispatcherが取得できます