開発環境
Visual Studio 2013、.NET Framework 4.6 で WPF アプリの開発をしています。
質問内容
下記のようなコードを Release ビルドし、
exe から単体で起動したところ、
ウィンドウを閉じた後に GC をしているにも関わらず、
ウィンドウの生存が確認されました。
最新の動作確認用コードは一番下にあります。
WM_DESTROY を処理しているにも関わらず GC で回収されないことがあります。
XAML
1<!-- App.xaml --> 2<!-- StartupUri プロパティの指定を削除しただけです --> 3<Application x:Class="WpfApplication3.App" 4 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 6 <Application.Resources> 7 8 </Application.Resources> 9</Application>
C#
1// App.xaml.cs 2namespace WpfApplication3 3{ 4 using System; 5 using System.Windows; 6 7 /// <summary> 8 /// App.xaml の相互作用ロジック 9 /// </summary> 10 public partial class App : Application 11 { 12 protected override void OnStartup(StartupEventArgs e) 13 { 14 base.OnStartup(e); 15 16 this.ShutdownMode = ShutdownMode.OnExplicitShutdown; 17 18 var w = new MainWindow(); 19 20 // 弱参照でウォッチ 21 _r = new WeakReference<object>(w); 22 23 // ウィンドウを開いて閉じる 24 w.Show(); 25 w.Close(); 26 w = null; 27 this.MainWindow = null; 28 29 // GC を強制的におこなう 30 GC.Collect(); 31 GC.WaitForPendingFinalizers(); 32 GC.Collect(); 33 MessageBox.Show("GC.Collect() しました。"); 34 35 // 生存確認 36 object obj; 37 if (_r.TryGetTarget(out obj)) 38 { 39 MessageBox.Show(obj + " を参照中。"); 40 } 41 else 42 { 43 MessageBox.Show("参照していません。"); 44 } 45 46 // 終了 47 this.Shutdown(); 48 } 49 50 private WeakReference<object> _r; 51 } 52}
XAML
1<!-- MainWindow.xaml --> 2<!-- 初期状態から何も変更していません --> 3<Window x:Class="WpfApplication3.MainWindow" 4 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 6 Title="MainWindow" Height="350" Width="525"> 7 <Grid> 8 9 </Grid> 10</Window>
あっさり GC してくれると思っていましたが、
それほど単純ではないようです。
さらに上記のコードの内、
C#
1 //w.Show(); 2 //w.Close();
というように、表示すらしないようにしても GC されないようです。
一体誰がどこで MainWindow を参照し、GC を妨げているのか教えてください。
途中経過
調査している中で、App.Windows プロパティが怪しいかと思いましたが、そうではありませんでした。
このプロパティは「アプリケーションでインスタンス化されたウィンドウを取得」できるようです。
このコレクションは読み取り専用で、クリアしたり各要素に null を代入したりすることができないようです。
ただし、Window.Close() メソッドが呼ばれるとそのコレクションから参照が削除されます。参考ソース
よって、App.Windows プロパティに関しては Close() メソッドさえ呼べば問題ないということがわかりました。
App.Shutdown() を呼んだ後、OnExit() メソッドをオーバーライドして参照を確認したところ、
見事 GC によって破棄されていました。
ソースを確認すると、Dispatcher.BeginInvoke() によって OnExite() メソッドを非同期的に呼び出しているため、これによって後述する状況と同じことになり、たまたま Shutdown() で GC されるようになっただけと言えそうです。
2019/3/22 13:50 現在、皆様のおかげでここまできました。
現在下記のコードにて MainWindow が GC によって回収されることが確認できています。
C#
1namespace WpfApplication3 2{ 3 using System; 4 using System.Threading.Tasks; 5 using System.Windows; 6 7 /// <summary> 8 /// App.xaml の相互作用ロジック 9 /// </summary> 10 public partial class App : Application 11 { 12 protected override async void OnStartup(StartupEventArgs e) 13 { 14 base.OnStartup(e); 15 16 this.ShutdownMode = ShutdownMode.OnExplicitShutdown; 17 18 var w = new MainWindow(); 19 20 // 弱参照でウォッチ 21 var r = new WeakReference<object>(w); 22 23 // ウィンドウを開いて閉じる 24 w.Show(); 25 w.Close(); 26 w = null; 27 28 // 時間をおいてから GC を強制的におこなう 29 await Task.Delay(10); 30 GC.Collect(); 31 GC.WaitForPendingFinalizers(); 32 GC.Collect(); 33 34 // WM_DESTROY の確認 35 MessageBox.Show("IsDestroy = " + IsDestroy); 36 37 // 生存確認 38 object obj; 39 if (r.TryGetTarget(out obj)) 40 { 41 MessageBox.Show(obj + " を参照中。"); 42 } 43 else 44 { 45 MessageBox.Show("参照していません。"); 46 } 47 48 // 終了 49 this.Shutdown(); 50 } 51 52 public static bool IsDestroy { get; set; } 53 } 54}
MainWindow の Close() メソッドを呼んでから GC.Collect() を呼ぶまでの間に、非同期的に時間を置くことで GC されるようになるようです。
上記のコードでは 10[ms] しか待っていないため、10 回に 4 回ほどの割合でしか GC されません。Task.Delay() の時間を(私の環境では 100[ms] に)延ばすとほぼ確実に GC されるようになりました。
このことをもう少し詳しく調査するため、WM_DESTROY を捕捉してそのメッセージが投げられたかどうかを確認しましたが、WM_DESTROY は必ず投げられるようで、上記の "IsDestroy = " のメッセージボックスは常に true となりました。それでも MainWindow が GC されるときとされないときがあることを確認しました。
このときの MainWindow.xaml.cs のコードは次のようになります。
C#
1// MainWindow.xaml.cs 2namespace WpfApplication3 3{ 4 using System; 5 using System.Runtime.InteropServices; 6 using System.Windows; 7 using System.Windows.Interop; 8 9 /// <summary> 10 /// MainWindow.xaml の相互作用ロジック 11 /// </summary> 12 public partial class MainWindow : Window 13 { 14 public MainWindow() 15 { 16 InitializeComponent(); 17 18 this.SourceInitialized += OnSourceInitialized; 19 } 20 21 private void OnSourceInitialized(object sender, EventArgs e) 22 { 23 // メッセージ処理をフック 24 var handle = (new WindowInteropHelper(sender as Window)).Handle; 25 var hwndSource = HwndSource.FromHwnd(handle); 26 hwndSource.AddHook(WndProc); 27 } 28 29 private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) 30 { 31 if (msg == WM_DESTROY) 32 { 33 App.IsDestroy = true; 34 } 35 36 return IntPtr.Zero; 37 } 38 39 private const int WM_DESTROY = 0x0002; 40 } 41}
まとめ
これまでの調査によって、以下のことが明確になりました。
- Window クラスを new するとその参照が Application.Windows コレクションに追加される
- Window.Close() メソッドを呼ぶか Application.Shutdown() メソッドを呼ぶとApplication.Windows コレクションから参照が除外される
したがって、new した Window クラスへの参照は、ただ null を代入しても GC されずに残るため、必ず Close() メソッドを呼ぶ必要がある。ただし、「他からの参照」が生き残っているため、Close() メソッドを呼んだだけでは GC の対象にはならない。ある程度時間を(今回の私の環境では 100[ms])置くと「他からの参照」がなくなり、GC の対象となる。
この質問スレッドもだいぶ文章が長くなってきましたので、「他からの参照」というのが何者なのかについてはここでは追究しません。
今後の調査によってはまた質問を投稿させていただきますので、そのときはまた皆様にご協力いただきたいと思います。中々に濃い内容だったように思いますが、これまでお付き合いいただきありがとうございました。
回答4件
あなたの回答
tips
プレビュー