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。作者声称编译器创建了单独的变量来保存将增加引用计数器的值。恕我直言,这是错误的,第一个例子证明了这一点。那是对的吗?

任何帮助都会很棒!

4

5 回答 5

48

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

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

长答案:

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

附带说明:您应该始终在使用完 COM 对象后立即释放它们。将这项工作留给 GC 可能会导致泄漏、意外行为和事件死锁。如果您访问的对象不在创建对象的 STA 线程上,则这一点要重要十倍。[Ref 2] [Ref 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's - RCW 引用计数规则!= COM 引用计数规则

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

3. Eran Sandler - Marshal.ReleaseComObject 和 CPU Spinning

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

于 2011-04-24T15:08:22.167 回答
3

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

当系统第一次看到一个 COM 接口指针时,它只是去缓存看看是否有那个接口指针的 RCW。大概缓存将使用弱引用,以免阻止 RCW 的最终确定和收集。

如果该指针有一个实时包装器,系统将返回该包装器——如果接口是以增加接口引用计数的方式获得的,那么 RCW 系统可能会在此时调用 Release()。它找到了一个活动的包装器,所以它知道包装器是一个单一的引用,并且它想只维护一个引用。如果缓存中没有实时包装器,它会创建一个新包装器并将其返回。

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

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

不幸的是,我对 RCW 系统如何处理代理对象生成以跨应用程序域或线程单元发送内容没有很好的理解。这不是我需要为 SlimDX 复制的系统的一个方面。

于 2011-01-11T16:56:14.067 回答
1

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

你不需要手动调用 Marshal.ReleaseComObject 除非你想要更确定的释放。

于 2011-01-04T08:30:20.457 回答
1

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

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

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

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

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

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

您可以使用Marshal.GetComInterfaceForObject().

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

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

int comObjectReferenceCount(object comObject)
{
    var iUnknown = Marshal.GetIUnknownForObject(comObject);
    Marshal.GetObjectForIUnknown(iUnknown);
    Marshal.Release(iUnknown);
    return Marshal.ReleaseComObject(comObject);
}
于 2017-01-20T11:49:07.980 回答
-2

您需要调用Marshal.ReleaseComObject您的 wrd 变量来释放您对单词应用程序的引用。

这样,如果 Word 不可见并且您关闭了应用程序,则该 exe 也将卸载,除非您已使其对用户可见。

于 2011-01-04T08:24:39.497 回答