A certain engineer "COMPLEX"

UWPメモ その5 UWP/WPFで画像オブジェクトに対する解放処理は不要なのか

会社で使う学習資料の更新が終わりました。毎回PowerPointのスライド20枚とサンプルプログラム1,2個つけたのが、今日の時点で13個。頑張ったものです。

Problem


WinFormsだと、画像オブジェクト、例えばSystem.Drawing.Bitmapは不要になったら、nullで参照を削除する前に、Disposeメソッドを呼び出す必要があります。
これは内部で保持している、GDIによるアンマネージドデータを破棄するために他なりません。

対して、GDIを使っていないWPF/UWPはどうなのでしょう?
無限ループでSystem.Windows.Media.Imaging.WriteableBitmapを生成するプログラムを組んでチェックしましたが、private working setが増えていたことから、アンマネージドでどこかにデータを確保しているのは間違いありませんでした。

でも、IDiposableインターフェイスは実装していません。
本当に参照が全部消えれば破棄されるの?
不安はぬぐい去れません。

五年前、入社二年目の私はインド人が手を入れた.NETをプログラムの修正を任されました。当時.NETを扱える人間が私しかいなかったためです。
で、そのソース、画像データを大量に扱う代物で、一切解放処理が書いてありませんでした。
当然しばらくするとメモリ不足で落ちました。4000x4000とか巨大なビットマップを扱うものですから...
トルコまで行って対応したのはいい思い出です(白目)

Inspection


簡単なサンプルプログラムをWPF/UWPそれぞれ用意しました。

https://github.com/takuya-takeuchi/Demo/tree/master/UWP4

UWP側のViewModelだけ記載します。


using System;
using System.Collections.Generic;
using Windows.UI.Xaml.Media.Imaging;
using Prism.Commands;
using Prism.Windows.Mvvm;
using Shared.Services;
using Shared.ViewModels;

namespace UWP4.ViewModels
{
public sealed class MainPageViewModel : ViewModelBase, IMainPageViewModel
{

private readonly List<WriteableBitmap> _WriteableBitmaps = new List<WriteableBitmap>();

private readonly IDispatcherService _DispatcherService;

public MainPageViewModel(IDispatcherService dispatcherService)
{
this._DispatcherService = dispatcherService;
this.Width = 2000;
this.Height = 2000;
this.Count = 10;
}

private int _Count;
public int Count
{
get
{
return this._Count;
}
set
{
this.SetProperty(ref this._Count, value);
}
}

private int _Width;
public int Width
{
get
{
return this._Width;
}
set
{
this.SetProperty(ref this._Width, value);
}
}

private int _Height;
public int Height
{
get
{
return this._Height;
}
set
{
this.SetProperty(ref this._Height, value);
}
}

private bool _Freeze;
public bool Freeze
{
get
{
return this._Freeze;
}
set
{
this.SetProperty(ref this._Freeze, value);
}
}

private DelegateCommand _StartCommand;
public DelegateCommand StartCommand
{
get
{
if (this._StartCommand == null)
{
this._StartCommand = new DelegateCommand(
() =>
{
var width = this.Width;
var height = this.Height;
var count = this.Count;
for (var i = 0; i < count; i++)
this._WriteableBitmaps.Add(new WriteableBitmap(width, height));
});
}

return this._StartCommand;
}
}

private DelegateCommand _ClearCommand;
public DelegateCommand ClearCommand
{
get
{
if (this._ClearCommand == null)
{
this._ClearCommand = new DelegateCommand(
() =>
{
this._WriteableBitmaps.Clear();
});
}

return this._ClearCommand;
}
}

private DelegateCommand _GCCommand;
public DelegateCommand GCCommand
{
get
{
if (this._GCCommand == null)
{
this._GCCommand = new DelegateCommand(GC.Collect);
}

return this._GCCommand;
}
}

}

}

Startボタンを押下すると、TextBoxで指定した、Width、HeightとCountで指定しただけのWriteableBitmapを生成して、System.Collections.Generic.Listに格納していきます。
Clearボタンを押下すると、List.Clearを呼びリストを空にします。

この時点で参照が全て消えるので、GCが動けばメモリが解放されるはずです。
一応明示的にGCを呼ぶためのGCボタンを用意しました。
また、WPFのWriteableBitmapはSystem.Windows.Freezableクラスの派生形であるため、Freezeメソッドをコールした場合、そうでない場合、挙動がどう変化するのかを確認します。

Try


計測は、dotMemoryを使います。
まずUWPです。
起動直後、Start直後、Clear直後、GC直後の4段階でSnapshotを採取します。

次に、Start直後とClear直後で比較をとります。
Listに含まれるWriteableBitmapのインスタンスが削除されていることがわかりますが、この時点ではメモリ量は変化しません。
最後の、GC直後の段階でメモリ使用量が大幅に減っているのがわかります。

同じようにWPFでもテストします。最初はFreezeしない状態で4つのSnapshotを採取します。

最後に、起動直後にFreezeのフラグを有効にして、WriteableBitmap生成直後にFreezeする処理を追加したパターンで4つのSnapshotを採取します。

Freezeの有無でメモリ使用量に変化があります。Freezeすると確保するメモリ量が60%位になりました。
Freeze状態にすると変更できなくなるため、データ内容を変更するために用意したバッファなどがなくなるのでしょうか?
どちらにしても変更が不要なら、不要になった時点で即座にFreezeした方が良さそうです。

UWPも219.67MB-66.69MB=152.98MB
WPFも230.71MB-77.42MB=153.29MB

となり、

200020004(ARGB?)*10/1024/1024=152MB

というデータ量に近似します。

メモリリークは発生していないと言えそうです。

Conclusion


WinFormsと異なり、画像データのメモリリークは起きないことが証明されました。
ただ、WinFormsとは違って、GCが発生しないとメモリが解放されないのはどうも...
明示的にGCを呼ぶのはWinFormsの時の経験から言って避けたいところです。明示的に呼んでフリーズしてそのまま何もできなくなってしまったことがあったので。

なにはともあれ、不安は消えました。

Source Code


https://github.com/takuya-takeuchi/Demo/tree/master/UWP4

コメントを残す

メールアドレスが公開されることはありません。

%d人のブロガーが「いいね」をつけました。