2012-01-27 46 views
10

我有一個具有Master-Details視圖的應用程序。當您從「主」列表中選擇一個項目時,它會使用一些圖像(通過RenderTargetBitmap創建)填充「詳細信息」區域。RenderTargetBitmap GDI處理Master-Details視圖中的泄漏

每次我從列表中選擇一個不同的主項目時,我的應用程序使用的GDI句柄的數量(如Process Explorer中報告的)會增加 - 並且最終在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> 

如果您單擊列表中的第一個項目,然後按住Down光標鍵,則可以重現該問題。

從看着!在WinDbg中的gcroot與SOS,我找不到任何保持RenderTargetBitmap對象活着的任何東西,但如果我做了!dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap,它仍然顯示尚未收集的數千個RenderTargetBitmap對象。

回答

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虛擬化列表的能力(但您可能必須至少定義包含圖像的高度和數量,以便它可以正確管理滾動條)。我建議返回一個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); 
     } 
    } 
} 

主要的事情這使運行時,據我所看到的,是(項目數這個枚舉顯然不會),也就是說它既不需要枚舉它多次,也不必猜(!)。我可以在光標鍵上用手指在列表中上下移動,即使有1000個MasterItem,也不會吹出10k的手柄,所以對我來說看起來很不錯。 (我的代碼沒有明確的GC)

+0

請注意,我也嘗試緩存ObservableCollection也。不幸的是,持有這個集合似乎最終也能保持GDI的處理。 – 2012-01-29 21:22:03

+0

謝謝,這很好。它確實解決了示例應用程序的問題,我只需要嘗試將其融入真正的應用程序。我不確定爲什麼ObservableCollection幫助這裏。如果僅僅是因爲大小,那麼列表應該具有相同的效果。 – Wilka 2012-01-30 10:26:17

2

如果你克隆到一個更簡單的位圖類型(和凍結)它不會佔用盡可能多的gdi句柄,但它會更慢。 有通過序列化克隆How achieve Image.Clone() in WPF?"

+0

WriteableBitmap有一個採用BitmapSource的ctor,因此克隆它的速度更快,同時也解決了這個問題。謝謝。 – Wilka 2012-01-30 10:58:29