我有一个使用此ListView的WPF应用程序:
<ListView ItemsSource="{Binding Log.Entries}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
<i:Interaction.Behaviors>
<afutility:AutoScrollBehavior/>
</i:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Foreground="{Binding Path=LogLevel, Mode=OneTime, Converter={StaticResource LogLevelToBrushConverter}}"
Text="{Binding Path=RenderedContent, Mode=OneTime}"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Log.Entries结束于这个类的一个示例(List本身永远不会被替换,它始终是同一个对象):
public class RingList<T> : IList<T>, INotifyCollectionChanged, INotifyPropertyChanged
该类本质上是一个自定义列表,其内容上限为100项。添加一个达到最大容量的项会从头部删除一个项。对于每个添加/删除的项,我调用CollectionChanged
如下:
// For added items
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
// For removed items (only ever removes from the start of the ring)
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, headItem, 0));
集合中的项不是对象,它们是如下所示的结构:
public struct RecordedLogEntry : IEquatable<RecordedLogEntry> {
public string RenderedContent { get; set; }
public Level LogLevel { get; set; }
// [...] Equals, GetHashCode, ToString, etc omitted for brevity, all standard
}
我知道,绑定到非INotifyPropertyChange对象可能会导致内存泄漏(请参见:(第10页)
这就是为什么我使用Mode=OneTime
来(希望)避免这种情况。然而,剖析使用的是不同的语言。
这是在运行时经过几个小时的工作后进行的内存转储,如果不进行处理,通常会导致系统内存不足:
可以清楚地看到:
- 有上限集合中的100个项目
- 再往上一点,当前可见的ListViewItem示例引用了12个项(由于列表视图正在虚拟化项,这大约是预期的数量)
- ListView本身引用的示例超过700k
- 随着时间的推移而累积的大量数据
项目在Windows上使用. NET 4.7.2。
如何避免这种泄漏?
编辑,忽略此要求:
理想情况下,我不想改变结构体,因为我在后台产生了许多这样的项(并不是所有的项都显示在100个项中),所以我想保持日志条目的占用空间很小。
正如@Joe正确指出的那样,这是不成熟的优化。事实仍然是,这些日志条目并不纯粹用于UI显示,而是用于其他地方。
它们都不会在生存期内更改内容,因此让实现通知更改似乎违反直觉。
是否有一种方法可以使绑定不关心更新,并在此用例中进行真正的一次性绑定,或者是否只有一种方法可以添加 Package 类/将数据复制到实现INotifyPropertyChange
的类中,以便消除内存泄漏?
1条答案
按热度按时间6ie5vjzr1#
不幸的是,这个问题不能用最小的例子重现,所以我不得不假设这些示例在其他地方保持活动状态。
一个最小的例子,通常可以很容易地显示泄漏,如果它来自绑定:
后面的代码:
产品型号:
RingList类:
集合的项目:
使用"直接"日志垃圾邮件启动模拟,即直接写入
Entries
不会导致泄漏。它消耗大量内存,但GC能够清除它:
使用"Indirect"日志垃圾启动模拟,即在每500ms循环一次的缓冲区中收集条目,似乎会导致内存泄漏。GC不会立即清除它:
但最终还是会的,只是需要更长的时间:
结论:不是绑定中的泄漏,更可能是某个缓冲区或保持示例活动的其他位置中的无限增长。
旁注:从类更改为结构体不会对情况产生显著影响(可能会更改总体消耗,但不会导致或解决问题)。