在C#中使用COM互操作时的RCW和引用计数

我有一个使用Office互操作程序集的应用程序。 我知道运行时管理的“运行时可调用包装器(RCW)”。 但我不太确定引用计数如何增加。 MSDN说,

RCW只保留对包装的COM对象的一个​​引用,而不管调用它的受管客户端的数量。

如果我理解正确,在以下示例中,

using Microsoft.Office.Interop.Word; static void Foo(Application wrd) { /* .... */ } static void Main(string[] args) { var wrd = new Application(); Foo(wrd); /* .... */ } 

我将实例wrd传递给另一个方法。 但这不会增加内部引用计数。 所以我想知道引用计数增加的场景是什么? 任何人都可以指出引用计数增加的情况吗?

另外我读了一些博客,说在使用COM对象编程时避免使用双点。 像wrd.ActiveDocument.ActiveWindow这样的东西。 作者声称编译器创建单独的变量来保存将增加引用计数器的值。 恕我直言,这是错误的,第一个例子certificate了这一点。 那是对的吗?

任何帮助都会很棒!

我一直在研究这个问题,研究COM / .Net-Interop为中心的应用程序,防止泄漏,挂起和崩溃。

简短回答:每次COM对象从COM环境传递到.NET。

答案很长:

  1. 对于每个COM对象,有一个RCW对象[测试1] [参考4]
  2. 每次从COM对象中请求对象时,引用计数都会递增(在返回COM对象的COM对象上调用属性或方法,返回的COM对象引用计数将增加1)[测试1]
  3. 通过强制转换到对象的其他COM接口或移动RCW参考[Test 2],不会增加引用计数
  4. 每次将对象作为COM引发的事件中的参数传递时,引用计数都会递增[参考1]

另请注意:您应该在完成使用后立即释放COM对象。 将此工作留给GC可能会导致泄漏,意外行为和事件死锁。 如果您访问的对象不是在创建的STA线程上,那么这十分重要。 [参考2] [参考3] [痛苦的个人经历]

我希望我已经涵盖了所有案例,但COM是一个艰难的cookie。 干杯。

测试1 – 参考计数

 private void Test1( _Application outlookApp ) { var explorer1 = outlookApp.ActiveExplorer(); var count1 = Marshal.ReleaseComObject(explorer1); MessageBox.Show("Count 1:" + count1); var explorer2 = outlookApp.ActiveExplorer(); var explorer3 = outlookApp.ActiveExplorer(); var explorer4 = outlookApp.ActiveExplorer(); var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer4); var count2 = Marshal.ReleaseComObject(explorer4); MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals); } Output: Count 1: 4 Count 2: 6, Equals: True 

测试2 – 参考计数续。

 private static void Test2(_Application outlookApp) { var explorer1 = outlookApp.ActiveExplorer(); var count1 = Marshal.ReleaseComObject(explorer1); MessageBox.Show("Count 1:" + count1); var explorer2 = outlookApp.ActiveExplorer(); var explorer3 = explorer2 as _Explorer; var explorer4 = (ExplorerEvents_10_Event)explorer2; var explorerObject = (object)explorer2; var explorer5 = (Explorer)explorerObject; var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer5); var count2 = Marshal.ReleaseComObject(explorer4); MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals); } Output: Count 1: 4 Count 2: 4, Equals: True 

除了我的经验和测试之外,我继续介绍的来源:

1. Johannes Passing的 – RCW参考计数规则!= COM参考计数规则

2. Eran Sandler – Runtime Callable Wrapper Internals和常见的陷阱

3. Eran Sandler – Marshal.ReleaseComObject和CPU Spinning

4. MSDN – 运行时可调用包装器

我还没有看到RCW的代码 – 甚至不确定它是否是SSCLI的一部分 – 但是我必须在SlimDX中实现一个跟踪COM对象生命周期的类似系统,并且必须对RCW进行一些研究。 这是我记得的,希望它是相当准确的,但要带上一点盐。

当系统第一次看到COM接口指针时,它只是进入高速缓存以查看该接口指针是否存在RCW。 据推测,缓存将使用弱引用,以免阻止RCW的最终化和收集。

如果该指针有一个实时包装器,系统将返回包装器 – 如果以增加接口引用计数的方式获取接口,则可能RCW系统此时将调用Release()。 它找到了一个实时包装器,因此它知道包装器是一个引用,并且它想要保持一个引用。 如果缓存中没有实时包装器,它会创建一个新包装并返回它。

包装器从终结器调用底层COM接口指针上的Release。

包装器位于您和COM对象之间,并处理所有参数封送处理。 这也允许它允许它获取任何接口方法的原始结果,该接口方法本身是另一个接口指针,并且在返回包装的接口指针之前,通过RCW高速缓存系统运行该指针以查看它是否存在。

不幸的是,我不太了解RCW系统如何处理代理对象生成以跨应用程序域或线程公寓发送内容; 它不是我需要为SlimDX复制的系统的一个方面。

你不应该需要任何特殊的治疗。 运行时只保留对COM对象的一个​​引用。 原因是GC跟踪所有托管引用,因此当RCW超出范围并被收集时,将释放COM引用。 传递托管引用时,GC会为您跟踪它 – 这是基于GC的运行时相对于旧的AddRef / Release方案的最大优势之一。

除非您需要更多确定性释放,否则无需手动调用Marshal.ReleaseComObject。

接受的解决方案是有效的,但这里有一些额外的背景信息。

RCW在其内部为其COM对象包含一个或多个本机COM对象接口引用。

当RCW释放其底层COM对象时,无论是由于收集垃圾还是由于Marshal.ReleaseComObject()被调用,它都会释放其内部持有的COM对象接口。

实际上有很多引用计数 – 一个确定.NET的RCW何时应该释放它的底层COM对象接口,然后每个原始COM接口都有自己的引用计数,就像在常规COM中一样。

这是获取原始COM IUnknown接口引用计数的代码:

 int getIUnknownReferenceCount(object comobject) { var iUnknown = Marshal.GetIUnknownForObject(comObject); return Marshal.Release(iUnknown); } 

您可以使用Marshal.GetComInterfaceForObject()为对象的其他COM接口获得相同的内容。

除了在接受的解决方案中列出的方法之外,我们还可以通过调用类似Marshal.GetObjectForIUnknown()人工增加.NET RCW引用计数。

下面是使用该技术获取给定COM对象的RCW引用计数的示例代码:

 int comObjectReferenceCount(object comObject) { var iUnknown = Marshal.GetIUnknownForObject(comObject); Marshal.GetObjectForIUnknown(iUnknown); Marshal.Release(iUnknown); return Marshal.ReleaseComObject(comObject); } 

您需要在wrd变量上调用Marshal.ReleaseComObject以释放对单词application的引用。

这样,如果Word不可见并且你关闭了你的应用程序,exe也会卸载,除非你让它对用户可见。