C# · 12月 29, 2021

c# – ReactiveUI绑定似乎阻止垃圾回收发生

我们正在使用ReactiveUI帮助构建一个相当大的基于 WPF的 Windows应用程序.一切顺利,直到我们发现我们的应用程序正在消耗大量的内存…基本上我们所有的观点,视图模型和模型都没有被垃圾回收.

基于内存分析器的信息,如Jet Brains dotMemory,ReactiveUI似乎是主要的祸根.特别是我们在我们的视图中配置的ReactiveUI绑定,即使我们使用最佳实践,并确保在视图被禁用时所有绑定都被处理.

以下是我们正在创建的其中一个视图的示例.任何关于我们可能会出错的想法将不胜感激.

public partial class RunbookInputsView : IViewFor<RunbookInputsviewmodel>{ public static readonly DependencyProperty viewmodelProperty = DependencyProperty.Register( “viewmodel”,typeof(RunbookInputsviewmodel),typeof(RunbookInputsView)); public RunbookInputsView() { InitializeComponent(); this.WhenActivated(d => { d(this.OneWayBind(viewmodel,vm => vm.AddInput,v => v.AddInput.Command)); d(this.OneWayBind(viewmodel,vm => vm.Inputs,v => v.Inputs.ItemsSource)); }); } object IViewFor.viewmodel { get { return viewmodel; } set { viewmodel = (RunbookInputsviewmodel)value; } } public RunbookInputsviewmodel viewmodel { get { return (RunbookInputsviewmodel) GetValue(viewmodelProperty); } set { SetValue(viewmodelProperty,value); } }}解决方法 从这个问题来看,很难说泄漏来自哪里.让泄漏发生一段时间,然后附加到windbg( Debugging Tools For Windows的一部分)的过程(注意:您可能需要构建x86或x64才能使其工作.)

一旦附加,通过输入命令设置.net调试:

.symfixsxe clrsxd av.loadby sos clr

然后可以使用!dumpheap -stat来获取每种类型的内存使用情况.这产生以下格式的输出:(我截断了类名,列表中的可读性).

0:012> !dumpheap -statStatistics: MT Count TotalSize Class Name000007fefa55d2e8 1 24 System.[…]TransportSinkProvider000007fefa55ce08 1 24 System.Run[…]rtSinkProvider000007fee7c32df0 1 24 System.LocalDataStoreHolder000007fee7c2ff78 1 24 System.Colle[…]000007fee7c2ece0 1 24 System.Resources.FastResourceComparer000007fee7c2ead0 1 24 System.Resources.ManifestBasedResourceGroveler000007fee7c2ea70 1 24 System.[…]eManagerMediator000007fee4cc1b70 4 1216 System.Xml.XmlDocument

如果您有内存泄漏,那么您将看到泄漏的对象. (应该有很多它们).一旦你确定了什么泄漏,你可以做一个!dumpheap -type来获取一个实际对象的列表. (对于本例,我将使用System.Xml.XmlDocument.类型名称区分大小写,必须是完全限定的.)

0:012> !dumpheap -type System.Xml.XmlDocument Address MT Size0000000002af9050 000007fee4cc1b70 304 0000000002afa628 000007fee4cc1b70 304 0000000002b0ea30 000007fee4cc1b70 304 00000000037e2780 000007fee4cc1b70 304 Statistics: MT Count TotalSize Class Name000007fee4cc1b70 4 1216 System.Xml.XmlDocument

您的列表可能会更大,但是概率表示泄漏类型的任何随机实例将是您感兴趣的事情.如果我们在其中一个地址上执行操作,我们将获得类似于此的输出:

0:012> !do 2af9050Name: System.Xml.XmlDocumentMethodTable: 000007fee4cc1b70EEClass: 000007fee4ae7f00Size: 304(0x130) bytesFile: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dllFields: MT Field Offset Type VT Attr Value Name000007fee4cc2b40 40004fc 8 System.Xml.XmlNode 0 instance 0000000000000000 parentNode000007fee4cc2258 400050a 10 …XmlImplementation 0 instance 0000000002af9180 implementation000007fee4cc22f0 400050b 18 ….Xml.DomNaMetable 0 instance 0000000002af92e0 domNaMetable[Entries removed for clarity]000007fee4cc26f0 400052f 108 …m.Xml.XmlResolver 0 instance 0000000000000000 resolver000007fee7c18c48 4000530 126 System.Boolean 1 instance 0 bSetResolver000007fee7c113e8 4000531 110 System.Object 0 instance 0000000002af9788 objLock000007fee4cc11b0 4000532 118 ….Xml.XmlAttribute 0 instance 0000000000000000 namespaceXml

您可以对表中列出的任何对象使用!做更多的信息.类似System.String和System.Boolean将会吐出它们的实际值.如果从创建的对象不清楚,下一步可能是使用!gcroot -nostacks来查找对象的引用.

0:012> !gcroot -nostacks 2af9050HandleTable: 00000000006117d8 (pinned handle) -> 0000000012a55748 System.Object[] -> 0000000002af9050 System.Xml.XmlDocumentFound 1 unique roots (run ‘!GCRoot -all’ to see all roots).

有更多的命令,这已经太长了. !help命令提供了一个很好的列表. (要使用任何一个,您需要在命令前缀!!!help [command]提供有关特定命令的详细信息,例如!help dumpobj:

0:012> !help dumpobj——————————————————————————-!DumpObj [-nofields] <object address>This command allows you to examine the fields of an object,as well as learn important properties of the object such as the EEClass,the MethodTable,and the size.You might find an object pointer by running !DumpStackObjects and choosingfrom the resultant list. Here is a simple object: 0:000> !DumpObj a79d40 Name: Customer MethodTable: 009038ec EEClass: 03ee1b84 Size: 20(0x14) bytes (C:\pub\unittest.exe) Fields: MT Field Offset Type VT Attr Value Name 009038ec 4000008 4 Customer 0 instance 00a79ce4 name 009038ec 4000009 8 Bank 0 instance 00a79d2c bankNote that fields of type Customer and Bank are themselves objects,and you can run !DumpObj on them too. You Could look at the field directly in memory usingthe offset given. “dd a79d40+8 l1” would allow you to look at the bank field directly. Be careful about using this to set memory breakpoints,since objectscan move around in the garbage collected heap.What else can you do with an object? You might run !GCRoot,to determine what roots are keeping it alive. Or you can find all objects of that type with “!DumpHeap -type Customer”.The column VT contains the value 1 if the field is a valuetype structure,and0 if the field contains a pointer to another object. For valuetypes,you can take the MethodTable pointer in the MT column,and the Value and pass them to the command !DumpVC.The abbreviation !do can be used for brevity.The arguments in detail:-nofields: do not print fields of the object,useful for objects like String