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

21

我有一个使用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。作者声称编译器会创建单独的变量来保存值,并增加引用计数。我认为这是错误的,第一个示例证明了这一点。这正确吗?
任何帮助都将是极好的!

Marshall.AddRef和Marshall.Release返回COM对象的新引用计数。不确定它有多准确,但至少您可以检查作者的说法。 - Arseny
5个回答

49

我也一直在研究这个问题,正在开发一个基于COM/.Net-Interop的应用程序,解决泄漏、挂起和崩溃的问题。

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

长回答:

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

另外:您应该始终在使用完COM对象后立即释放它们。将此工作留给GC可能会导致泄漏、意外行为甚至死锁。如果您访问在其创建时不在STA线程上的对象,这十倍重要。[Ref 2] [Ref 3] [令人痛苦的个人经历]

希望我已涵盖所有情况,但是COM确实很棘手。

Test 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内部和常见陷阱

3. Eran Sandler - Marshal.ReleaseComObject 和CPU自旋

4. MSDN - Runtime Callable Wrapper


1
这些测试并没有考虑到实际上有两个引用计数器,一个是COM对象的真正本地引用计数器(应该只由CLR增加一次),另一个是RCW内部的托管引用计数器,它计算从本地世界传递对象的次数(即您想调用ReleaseComObject的次数)。你的测试只测量了后者,这只是问题的部分答案。 - hypersw

3
我没有看过RCW的代码,甚至不确定它是否是SSCLI的一部分,但我在SlimDX中实现了一个类似的系统以跟踪COM对象的生命周期,并进行了大量的RCW研究。这是我记得的内容,希望它相当准确,但需要留心一些细节。
当系统首次看到一个COM接口指针时,它会去缓存中查找是否有该接口指针的RCW。假设缓存使用了弱引用,以便不会阻止RCW的终结和收集。
如果存在该指针的活动包装器,系统将返回该包装器——如果接口是通过增加接口的引用计数来获取的,则RCW系统在此时调用Release()方法。它已经找到了一个活动的包装器,因此它知道该包装器是单个引用,并且想要保持恰好一个引用。如果缓存中没有活动的包装器,则创建一个新的包装器并返回它。
包装器从终结器中对底层的COM接口指针调用Release()方法。
包装器位于您和COM对象之间,并处理所有参数的封送。这也允许它获取任何接口方法的原始结果,该结果本身是另一个接口指针,并将该指针通过RCW缓存系统运行,以查看它是否已经存在,然后再将包装的接口指针返回给您。
不幸的是,我并不了解RCW系统如何处理代理对象生成以跨应用程序域或线程单元发送数据;这不是我为SlimDX复制所需的系统方面。

1

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

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


问题不是关于Marshal.ReleaseComObject或类似的用法。我试图理解引用计数是如何工作的,以及在什么时候它会被增加。 - Navaneeth K N
1
没错。如果你再读一遍,你会注意到我确切地说了这个。只有一个COM引用。它永远不会增加。所有RCW的跟踪都由GC完成。 - codekaizen

1

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

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);
}

-2

你需要调用Marshal.ReleaseComObject来释放对Word应用程序的引用。

这样,如果Word不可见并且你关闭了应用程序,exe也会卸载,除非你已经将其显示给用户。


谢谢,但这并没有回答我的问题。我知道Marshal.ReleaseComObjectMarshal.FinalReleaseComObject方法。我的问题更多是关于内部引用计数是如何工作的以及在什么时候它会被增加的。 - Navaneeth K N

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接