RenderTargetBitmap GDI обрабатывает утечку в представлении Master-Details
У меня есть приложение с видом Master-Details. Когда вы выбираете элемент из "главного" списка, он заполняет область "деталей" некоторыми изображениями (созданными с помощью RenderTargetBitmap).
Каждый раз, когда я выбираю другой главный элемент из списка, количество дескрипторов GDI, используемых моим приложением (как сообщается в Process Explorer), увеличивается - и в конечном итоге падает (или иногда блокируется) при использовании 10000 дескрипторов GDI.
Я не знаю, как это исправить, поэтому любые предложения о том, что я делаю неправильно (или просто предложения о том, как получить больше информации), будут с благодарностью.
Я упростил свое приложение до следующего в новом приложении WPF (.NET 4.0) под названием "ThisThisLeak":
В 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
это все еще показывает несколько тысяч из них, которые еще не были собраны.
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)
Если вы клонируете в более простой растровый тип (и замораживаете), он не будет использовать столько же дескрипторов gdi, но будет медленнее. Там есть клонирование с помощью сериализации в ответ на Как достичь Image.Clone() в WPF?"
Попробуйте использовать решение, описанное здесь: RenderTargetBitmap.Render () генерирует исключение OutOfMemoryException при рендеринге больших визуальных изображений.
Обновление: также взгляните на утечку памяти RenderTargetBitmap.