12

マスター詳細ビューを備えたアプリがあります。「マスター」リストから項目を選択すると、「詳細」領域にいくつかの画像 (RenderTargetBitmap で作成) が入力されます。

リストから別のマスター アイテムを選択するたびに、アプリで使用されている GDI ハンドルの数 (プロセス エクスプローラーで報告されている) が増え、最終的には 10,000 個の GDI ハンドルでフォールオーバー (またはロックアップ) します。

私はこれを修正する方法について途方に暮れているので、私が間違っていることに関する提案 (またはより多くの情報を取得する方法に関する提案) をいただければ幸いです。

「DoesThisLeak」と呼ばれる新しい WPF アプリケーション (.NET 4.0) で、アプリを次のように簡略化しました。

MainWindow.xaml.cs 内

public partial class MainWindow : Window
{
    public MainWindow()
    {
        ViewModel = new MasterViewModel();
        InitializeComponent();
    }

    public MasterViewModel ViewModel { get; set; }
}

public class MasterViewModel : INotifyPropertyChanged
{
    private MasterItem selectedMasterItem;

    public IEnumerable<MasterItem> MasterItems
    {
        get
        {
            for (int i = 0; i < 100; i++)
            {
                yield return new MasterItem(i);
            }
        }
    }

    public MasterItem SelectedMasterItem
    {
        get { return selectedMasterItem; }
        set
        {
            if (selectedMasterItem != value)
            {
                selectedMasterItem = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedMasterItem"));
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class MasterItem
{
    private readonly int seed;

    public MasterItem(int seed)
    {
        this.seed = seed;
    }

    public IEnumerable<ImageSource> Images
    {
        get
        {
            GC.Collect(); // Make sure it's not the lack of collections causing the problem

            var random = new Random(seed);

            for (int i = 0; i < 150; i++)
            {
                yield return MakeImage(random);
            }
        }
    }

    private ImageSource MakeImage(Random random)
    {
        const int size = 180;
        var drawingVisual = new DrawingVisual();
        using (DrawingContext drawingContext = drawingVisual.RenderOpen())
        {
            drawingContext.DrawRectangle(Brushes.Red, null, new Rect(random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size));
        }

        var bitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
        bitmap.Render(drawingVisual);
        bitmap.Freeze();
        return bitmap;
    }
}

MainWindow.xaml 内

<Window x:Class="DoesThisLeak.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="900" Width="1100"
        x:Name="self">
  <Grid DataContext="{Binding ElementName=self, Path=ViewModel}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="210"/>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <ListBox Grid.Column="0" ItemsSource="{Binding MasterItems}" SelectedItem="{Binding SelectedMasterItem}"/>

    <ItemsControl Grid.Column="1" ItemsSource="{Binding Path=SelectedMasterItem.Images}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <Image Source="{Binding}"/>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </Grid>
</Window>

リストの最初の項目をクリックし、下向きカーソル キーを押したままにすると、問題を再現できます。

WinDbg で SOS を使用して !gcroot を調べたところ、これらの RenderTargetBitmap オブジェクトを維持するものは何も見つかりませんでしたが、!dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmapそれでもまだ収集されていない数千のオブジェクトが表示されます。

4

3 に答える 3

7

TL;DR: 修正済み。下を見てください。私の発見の旅と私が行ったすべての間違った路地を読んでください!

私はこれをいじり回しましたが、それ自体が漏れているとは思いません。これをループの両側に配置して GC を強化すると、次のようになります。

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

リストを (ゆっくりと) 下に移動すると、数秒後に GDI ハンドルに変化がないことがわかります。実際、MemoryProfiler でチェックすると、これが確認されます。アイテムからアイテムへとゆっくりと移動しても、.net または GDI オブジェクトはリークしません。

リストをすばやく下に移動すると問題が発生します-プロセスメモリが1.5Gを超え、GDIオブジェクトが壁にぶつかったときに10000に上昇するのを見ました。その後 MakeImage が呼び出されるたびに、COM エラーがスローされ、プロセスに対して何も実行できませんでした。

A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=SelectedMasterItem; DataItem='MasterViewModel' (HashCode=28657291); target element is 'ListBox' (Name=''); target property is 'SelectedItem' (type 'Object') COMException:'System.Runtime.InteropServices.COMException (0x88980003): Exception from HRESULT: 0x88980003
   at System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation()

これは、非常に多くの RenderTargetBitmaps がぶらぶらしている理由を説明していると思います。フレームワーク/GDIのバグであると仮定して、緩和戦略も提案します。レンダリング コード (RenderImage) をドメインにプッシュして、基になる COM コンポーネントを再起動できるようにしてください。最初は、独自のアパートメント (SetApartmentState(ApartmentState.STA)) でスレッドを試し、それが機能しない場合は、AppDomain を試しました。

ただし、非常に多くの画像を非常に迅速に割り当てるという問題の原因に対処する方が簡単です。9000 個の GDI ハンドルを取得して少し待っても、カウントはすぐに元に戻るからです。次の変更後のベースライン (COM オブジェクトにアイドル処理があり、数秒間何も必要とせず、すべてのハンドルを解放する別の変更があるように思えます)

これに対する簡単な修正はないと思います-スリープを追加して動きを遅くし、ComponentDispatched.RaiseIdle() を呼び出してみました-どちらも効果がありません。このように動作させる必要がある場合は、再起動可能な方法で GDI 処理を実行する (および発生するエラーに対処する) か、UI を変更する必要があります。

詳細ビューの要件、および最も重要な右側の画像の可視性とサイズに応じて、リストを仮想化する ItemsControl の機能を利用できます (ただし、少なくともスクロールバーを適切に管理できるように、含まれる画像の高さと数)。IEnumerable ではなく、画像の ObservableCollection を返すことをお勧めします。

実際、これをテストしたところ、次のコードで問題が解消されたようです。

public ObservableCollection<ImageSource> Images
{
    get 
    {
        return new ObservableCollection<ImageSource>(ImageSources);
    }
}

IEnumerable<ImageSource> ImageSources
{
    get
    {
        var random = new Random(seed);

        for (int i = 0; i < 150; i++)
        {
            yield return MakeImage(random);
        }
    }
}

これがランタイムに与える主なものは、私が見る限り、アイテムの数です(明らかに、列挙可能ではありません)。つまり、複数回列挙したり、推測したりする必要はありません(!)。1000 個の MasterItems があっても、この 10k ハンドルを吹き飛ばすことなく、カーソル キーに指を置いてリストを上下に移動できるので、私には良さそうです。(私のコードには明示的な GC もありません)

于 2012-01-29T21:16:19.147 に答える
0

ここで説明されているソリューションを使用してみてください: RenderTargetBitmap.Render() は、大きなビジュアルをレンダリングするときに OutOfMemoryException をスローします。

更新: また、RenderTargetBitmap Memory Leakもご覧ください。

于 2012-01-28T03:00:37.017 に答える