如何允许连接到COM对象的事件处理程序的对象进行垃圾回收

3
我有一个类,它提供了一个适配器来连接COM对象。这是一个简化版本的代码实现:
public class DocumentWrapper
{
    private COMDocument doc;
    public DocumentWrapper(COMDocument doc)
    {
        this.doc = doc;
        this.doc.OnClose += OnClose;
    }
    private void OnClose()
    {
        this.doc.OnClose -= OnClose;
        this.doc = null;
    }
    public bool IsAlive { get { return this.doc != null; } }
}

问题在于OnClose事件处理程序使两个对象都保持活动状态。这两个对象应该具有相同的生命周期,因此当一个对象消失时,另一个对象也应该消失,即没有人会保持一个对象的存活并期望另一个对象消失。
我尝试使用弱引用进行实验:
COMDocument com = CreateComDocument();
var doc = new DocWrapper(com);
WeakReference weak1 = new WeakReference(com);
WeakReference weak2 = new WeakReference(doc);
GC.Collect(2, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Forced);
if (weak1.Target != null)
    Console.WriteLine("COM was not collected");
if (weak2.Target != null)
    Console.WriteLine("Wrapper was not collected");

如果未注册OnClose,则两个对象都会被回收。如果注册了OnClose,则两个对象都不会被回收。如何确保这对对象可收集而不失去事件?

1
在将 this.doc 设为 null 之前,尝试调用 Marshal.ReleaseComObject(this.doc)。请务必先阅读此文:http://blogs.msdn.com/b/visualstudio/archive/2010/03/01/marshal-releasecomobject-considered-dangerous.aspx。 - noseratio - open to work
如果您控制服务器,可以通过让OnClose以参数形式接收文档来打破循环,这样事件处理程序就不必保留对它的引用。 - acelent
@Noseratio 在我的程序生命周期内,OnClose 可能永远不会被调用,它可能只会在用户关闭已连接的服务器应用程序时触发(这可能是在我的应用程序关闭之后很长一段时间)。 - Bryce Wagner
1
然后,你必须有一个额外的处理程序,将其注册到COMDocument,该处理程序对实际处理程序具有弱引用。如果弱引用不为null,则"弱"处理程序的OnClose将调用实际处理程序的OnClose。实际处理程序可以保留对COMDocument的引用。 - acelent
@acelent,你提到的弱引用处理程序是我最终采取的方法,同时我还在DocumentWrapper中添加了一个finalizer来释放弱引用处理程序。"private WeakReference<COMDocument>"显然行不通,因为那样就没有强引用了。 - Bryce Wagner
显示剩余9条评论
1个回答

1
实际解决方案更为复杂,因为我需要担心多个线程(终结器在后台线程上运行,注销COM事件必须调用到COM线程,如果不小心可能会触发死锁)。但是这里有一个基本的思路来构建事情,以确保它可以被垃圾回收:
public class DocumentWrapper
{
    private COMDocument doc;
    private WeakHandler closeHandler;
    public DocumentWrapper(COMDocument doc)
    {
        this.doc = doc;
        this.closeHandler = new WeakHandler(this);
    }
    ~DocumentWrapper()
    {
        if (closeHandler != null)
            closeHandler.Unregister();
    }
    public bool IsAlive { get { return this.doc != null; } }

    private class WeakHandler : IDisposable
    {
        private WeakReference owner;
        public WeakHander(DocumentWrapper owner)
        {
            this.owner = new WeakReference(owner);
            owner.doc.OnClose += Unregister();
        }
        private void Unregister()
        {
            if (owner == null)
                return;
            var target = owner.Target as DocumentWrapper;
            if (target != null && target.doc != null)
            {
                target.doc.OnClose -= OnClose;
                target.closeHandler = null;
                GC.SupressFinalize(target);
            }
            this.owner = null;
        }
    }
}

RCW已经是指向COM对象的对象,因此您只需要一个弱引用即可。无需进一步包装。实际上,您在这里所做的是让实际的DocumentWrapper被GC回收,即使COMDocument仍然存在。因此,在我的建议中,您只需要对COMDocument使用弱引用,而不需要进一步的间接引用,这实际上是有害的。 - acelent
在这个例子中,RCW 是“COMDocument”。实际的 COM 文档被隐藏在 RCW 后面。是的,我需要在“DocumentWrapper”内部对 COMDocument 有一个强引用,因为没有其他人持有它。如果只有一个弱引用,它将被垃圾回收。问题在于,由于事件处理程序,DocumentWrapper 从未被回收。使用上述代码后,它会被回收,并且当它被回收时,它将取消注册事件处理程序。 - Bryce Wagner
使用您当前的示例,DocumentWrapper 可能会在指向它的 COMDocument 之前(并且很可能会)被垃圾回收,因此您将错过从那时起的 OnClose 事件。您需要将弱引用尽可能接近 COM 对象。也就是说,您可能希望 COMDocument 的生存期与其他事物(除了事件处理程序之外)一样长。这就是为什么唯一的弱引用应该是对 RCW 的引用,而不是对实际事件处理程序的引用。 - acelent
重新思考了一下,我最终意识到你可能会保持对“DocumentWrapper”的强引用,并将“COMDocument”保持为私有。如果是这样的话,那么你的解决方案是正确的。 - acelent

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