Утечка обработки RenderTargetBitmap GDI в представлении Master-Details

У меня есть приложение с представлением Master-Details. Когда вы выбираете элемент из «основного» списка, он заполняет область «детали» некоторыми изображениями (созданными с помощью RenderTargetBitmap).

Каждый раз, когда я выбираю другой основной элемент из списка, количество дескрипторов GDI, используемых моим приложением (согласно отчету в Process Explorer), увеличивается и в конечном итоге падает (или иногда блокируется) при 10 000 используемых дескрипторов GDI.

Я не знаю, как это исправить, поэтому любые предложения о том, что я делаю неправильно (или просто предложения о том, как получить больше информации), будут очень признательны.

Я упростил свое приложение до следующего в новом приложении WPF (.NET 4.0) под названием «DoesThisLeak»:

В 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>

Вы можете воспроизвести проблему, если щелкнете по первому элементу в списке, а затем удержите клавишу курсора «Вниз».

Глядя на !gcroot в WinDbg с помощью SOS, я не могу найти ничего, что поддерживало бы эти объекты RenderTargetBitmap, но если я делаю !dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap, оно все равно показывает несколько тысяч из них, которые еще не были собраны.


person Wilka    schedule 27.01.2012    source источник


Ответы (3)


TL;DR: исправлено. См. дно. Читайте дальше о моем путешествии открытий и обо всех неправильных переулках, по которым я шел!

Я немного поковырялся с этим, и я не думаю, что он протекает как таковой. Если я усилю GC, поместив это по обе стороны цикла в изображения:

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

Вы можете пройти (медленно) вниз по списку и не увидеть никаких изменений в дескрипторах GDI через несколько секунд. Действительно, проверка с помощью MemoryProfiler подтверждает это — никакие объекты .net или GDI не протекают при медленном переходе от элемента к элементу.

У вас действительно возникают проблемы при быстром перемещении вниз по списку — я видел, как память процесса превысила 1,5 ГБ, а объект 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 перезапускаемым способом (и обработать возникающие ошибки) или изменить пользовательский интерфейс.

В зависимости от требований в подробном представлении и, что наиболее важно, от видимости и размера изображений в правой части, вы можете воспользоваться возможностью ItemsControl для виртуализации вашего списка (но вам, вероятно, придется по крайней мере определить высоту и количество содержащихся изображений, чтобы он мог правильно управлять полосами прокрутки). Я предлагаю вернуть ObservableCollection изображений, а не IEnumerable.

На самом деле, только что протестировав это, этот код, похоже, устраняет проблему:

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);
        }
    }
}

Насколько я вижу, главное, что это дает среде выполнения, — это количество элементов (которых перечислимое, очевидно, не имеет), что означает, что ему не нужно ни перечислять их несколько раз, ни угадывать (!). Я могу перемещаться вверх и вниз по списку, удерживая палец на клавише курсора, и при этом не сбрасывать 10 000 дескрипторов, даже с 1000 MasterItems, так что для меня это выглядит хорошо. (В моем коде также нет явного GC)

person James Ogden    schedule 29.01.2012
comment
Обратите внимание, я тоже пытался кэшировать ObservableCollection. К сожалению, хранение коллекции, по-видимому, в конечном итоге также содержит дескрипторы GDI. - person James Ogden; 30.01.2012
comment
Спасибо, это здорово. Это исправлено для примера приложения, теперь мне просто нужно попробовать встроить это в реальное приложение. Я не уверен, почему здесь помогает ObservableCollection. Если бы это было только из-за размера, то List‹T› должен иметь тот же эффект. - person Wilka; 30.01.2012

Если вы клонируете в более простой тип растрового изображения (и замораживаете), он не будет использовать столько дескрипторов gdi, но будет медленнее. Клонирование через сериализацию есть в ответе на вопрос Как добиться Image.Clone() в WPF?"

person marklam    schedule 30.01.2012
comment
WriteableBitmap имеет ctor, который принимает BitmapSource, поэтому его клонирование выполняется быстрее, а также устраняет проблему. Спасибо. - person Wilka; 30.01.2012

Попробуйте использовать решение, описанное здесь: RenderTargetBitmap.Render() генерирует исключение OutOfMemoryException при рендеринге больших визуальных элементов.

Обновление: также взгляните на Утечка памяти RenderTargetBitmap.

person Sergey Vyacheslavovich Brunov    schedule 28.01.2012