none
WPFでユーザーコントロールを破棄する方法 RRS feed

  • 質問

  • 例えば、WPFアプリケーションプロジェクトを作成し、以下のようなユーザーコントロールを定義したとします。

    <UserControl x:Class="WpfTest.MyControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:WpfTest"
                 mc:Ignorable="d" 
                 d:DesignHeight="300" d:DesignWidth="300">
        <Grid>
            <StackPanel HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top">
                <TextBlock TextWrapping="Wrap" Text="TextBlock"/>
                <TextBlock TextWrapping="Wrap" Text="TextBlock"/>
                <TextBlock TextWrapping="Wrap" Text="TextBlock"/>
            </StackPanel>
        </Grid>
    </UserControl>

    MainWindow.xaml.csから、このユーザーコントロールのインスタンス生成・破棄を繰り返したい場合、どのように実装すればよろしいでしょうか?
    試しに下記のようにしてみても、メモリリークが発生してしまいます。

    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                    control = null;
                }
            }
        }
    }

    Formアプリケーションの場合は、ユーザーコントロールをusing文で生成し、Dispose()メソッドの実行を保証しますが、WPFアプリケーションのユーザーコントロールはDispose()メソッドが実装されていないため、どのようにすれば良いのか分からずに困っています。

    • 編集済み Naoto K 2016年6月20日 3:48
    2016年6月20日 1:56

すべての返信

  • 強い参照もなさそうですし、本当にメモリリークでしょうか? とりあえず、GC.Collect() を実行してみるとどうでしょうか?

    (参考)
    ガベージ・コレクタを明示的に動作させるには?
    http://www.atmarkit.co.jp/fdotnet/dotnettips/021gc/gc.html


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年6月20日 3:11
    モデレータ
  • 余計な事でしたら申し訳ありませんが(trapemiyaさんがおっしゃっている通り、GC.Collect()を実行するとデストラクタが走り、メモリリークは無さそうと感じた為)、もしusingステートメントを利用されないことが不安であるようでしたら、MyControlにIDisposableインターフェースを実装するのはいかがでしょうか。

        public MainWindow()
         {
             InitializeComponent();
             for (int i = 0; i < 5000; i++)
             {
                 using (MyControl control = new MyControl())
                 {
                 }
             }
         }
    
         public partial class MyControl : UserControl, IDisposable
         {
             public MyControl()
             {
                 InitializeComponent();
             }
             public void Dispose()
             {
             }
         }

    2016年6月20日 3:40
  • ご回答ありがとうございます。
    GC.Collect()を使用するパターンをいくつか試してみましたが、GC.Collect()では改善はみられませんでした。

    ①ユーザーコントロールのインスタンスを何も生成しない下記コードの場合:使用プロセスメモリ=74MB

    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
        }
    }


    ②ユーザーコントロールのインスタンスを単純に生成する下記コードの場合:使用プロセスメモリ=89MB

    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                }
            }
        
    

    ③ユーザーコントロールを生成した後に毎回nullを代入する場合:使用プロセスメモリ=89MB

    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                    control = null;
                }
            }
        }
    }

    ④ユーザーコントロールのインスタンスを生成した後、最後にGC.Collect()を実行した場合:使用プロセスメモリ=94MB

    using System;
    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                }
    
                GC.Collect();
            }
        }
    }

    ⑤ユーザーコントロールを生成した後、毎回nullを代入し、最後にGC.Collect()を実行する場合:使用プロセスメモリ=94MB

    using System;
    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                    control = null;
                }
    
                GC.Collect();
            }
        }
    }

    2016年6月20日 3:45
  • ご回答ありがとうございます。

    ご提案くださったとおり、ユーザーコントロールのクラスにIDisposableインタフェイスを実装すれば、usingステートメントを使用することでDispose()メソッドが実行されますが、肝心のDispose()メソッド内での処理が空だと、Dispose()が実行されてもインスタンスが破棄されないのではないでしょうか?違ったらすみません。

    試しにご提案くださったコードで実行してみたところ、ループしてインスタンス生成するたびに使用メモリが増大してしまいました。ループごとにインスタンスを生成しても、同ループ内でインスタンスのリソースを破棄できていれば、使用メモリは生成インスタンス数に比例しないと思います。ループするたびに使用メモリが増大しているということは、インスタンスのリソースを破棄できていないということですよね?

    WPFのユーザーコントロールをDispose()メソッド内で明示的に破棄するには、どのようにすればよろしいでしょうか。



    • 編集済み Naoto K 2016年6月20日 4:00
    2016年6月20日 3:55
  • MainWindow に、UserControl と同じ種類のオブジェクトを張って、ユーザーコントロールを呼び出さない場合、呼び出した場合でサイズ計算してみてください。

    もしかしたら、コントロールのコードがロードされることで、メモリを消費しているだけじゃないか?という気がします。


    とっちゃん@わんくま同盟, Visual Studio and Development Technologies http://blogs.wankuma.com/tocchann/default.aspx

    2016年6月20日 4:09
  • 話をややこしくしてしまって申し訳ありません。

        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                    control = null;
                }
    
                GC.Collect();
            }
        }

    上記のメソッドが終了してから、GC.Collectを実施するとメモリの開放が確認できました。

    ですので、例えば画面にボタンを一つつけて、そこでGC.Collect()を実施してください。

    私の環境では、一気に20Mほど下がるのを確認できました。

    using云々は、開放しなければいけない何かが無ければ実装不要ですので忘れてやってください。
    • 編集済み runcs 2016年6月20日 4:20
    2016年6月20日 4:17
  • runcs様、ご回答ありがとうございます。

    MainWindow.xaml内でボタンを1つ配置し、ボタンクリック時にGC.Collect()を実行するようにしました。

    using System;
    using System.Windows;
    
    namespace WpfTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                for (int i = 0; i < 5000; i++)
                {
                    MyControl control = new MyControl();
                }
    
                GC.Collect();   // Point A
            }
    
            private void button_Click(object sender, RoutedEventArgs e)
            {
                GC.Collect();   // Point B
            }
        }
    }

    実行したところ、Point Aではメモリの解放は見受けられず、ボタンを押したときに実行されるPoint Bの時にはメモリの解放がされていました。
    ユーザーコントロールのインスタンスを何も生成していない場合の使用メモリ=74MBまで下がることを確認しました。

    MainWindowのコンストラクタ内でGC.Collect()を実行してもメモリが解放されないのが腑に落ちませんが…。

    2016年6月20日 4:40
  • point AのGC.Collectの前に、GC.WaitForPendingFinalizersメソッドを実施すれば、解放されそうです。何度も答えを変えてしまい、申し訳ありません。
    2016年6月20日 5:03
  • > WPFアプリケーションのユーザーコントロールはDispose()メソッドが実装されていないため、
    > どのようにすれば良いのか分からずに困っています。
     
    IDisposable インターフェイス を継承して Dispose メソッドを実装しているクラスは、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すべきと思いますが、そうでなければガベージコレクターに任せておけばよいと思います。

    MSDN ライブラリの「Dispose メソッドの実装」(URL 下記)を見ると、"マネージリソースのみを使用する型は、ガベージコレクターによって自動的にクリアされるため、このような型で Dispose メソッドを実装しても、パフォーマンス上の利点はありません" とのことです。(.NET 4.6 / 4.5 の記事にはその説明はありませんが同じことかと思います)

    Dispose メソッドの実装
    https://msdn.microsoft.com/ja-jp/library/fs2xkftw(v=vs.100).aspx

    Dispose メソッドには、メモリ開放の機能以外に、GC.SuppressFinalize メソッド を実装することにより、冗長なファイナライザーの呼び出しを防ぐことができるという利点があるそうです。

    MSDN フォーラム(URL 下記)で、Microsoft (MSFT) の方がそのあたりのことを述べてますし、上に紹介した MSDN ライブラリにもそのような記述があります。

    Should I call Dispose on a SQLCommand object?
    https://social.msdn.microsoft.com/Forums/en-US/916a1734-3e19-43a1-95c7-e3a2cd18369d/should-i-call-dispose-on-a-sqlcommand-object?forum=adodotnetdataproviders

    ただし、クラスによってはコンストラクタに GC.SuppressFinalize メソッドが実装されており、冗長な Finalize メソッドの呼び出しを防ぐという意味では Dispose() メソッドを呼ぶ必要はないものもあります。

    というわけで、ガベージコレクターに任せておけばよいと思います。メモリーリークがあるとと言われてますが、どうもそうではなさそうな感じですし。

    2016年6月20日 5:05
  • 質問者さんのプログラムは、結構長寿命なんじゃないでしょうか?

    そのようなプログラムで、GC.Collect()を多用すると、第二世代のオブジェクトが量産されて、そこらかしこでGC.Collect()を呼ばないと、メモリが無くならない限り解放されないオブジェクトだらけになりはしませんかね?

    また、サンプルは下手をするとReleaseビルドでループコードが消えるようなシロモノなので、サンプルの体をなしていないかと。

    実際は、イベントハンドラを解除していないとか、そう言うオチではありませんか?


    • 編集済み tmori3y2 2016年6月20日 5:40
    2016年6月20日 5:40