COM 接口 还有一个规则在上面的信息中没有提及,这就是每个 COM 接口都必须直接的或者间接的继承一个叫做 IUnknown 的接口类。这个接口为 COM 组件提供一些底层的功能支持。

IUnknown 接口定义了三个方法:

  • QueryInterface
  • AddRef
  • Release

QueryInterface 方法可以让一个程序在运行过程中查询对象具备的功能(或者说支持什么操作)。下一节内容我们深入讨论一下与该接口相关的内容。这节主要来介绍一下和 AddRef 和 Release 两个接口相关的主题,使用它们可以控制一个对象的生命周期。

1. 引用计数

一个程序不管用来做什么,它都将会涉及到资源的分配和释放。分配资源比较简单,但是什么时候释放资源确实一个难题,特别是资源的生命周期超出当前作用域的时候。

这个问题不仅仅 COM 组件独有,任何程序涉及到堆内存分配都必须解决相同的问题。例如,在 C++ 中可以使用构造函数,C# 和 Java 中可以使用垃圾回收(gc)。COM 组件 使用了一个称为引用计数(reference counting)的手法。

每个 COM 组件内部都维持一个叫做内部计数器(internal count)的东西,也被称为引用计数。

引用计数跟踪记录了当前有多少活动的对象引用,当这个数字为0的时候,这个对象将被删除。值得注意的是,程序不会主动的删除该对象,只会减少对象引用数,当值为0的时候,引用计数器(COM组件)会自动删除该对象。

下面是引用计数的规则:

  • 当对象第一次被创建的时候,引用计数记为1。这个时候程序有一个指向该对象的指针(或者说有一处引用)。

  • 程序中赋值这个指针,可以得到一个新的引用。当你拷贝这个指针的时候,你必须要调用 AddRef 方法通知这个对象,对象内部会将引用计数加1。

  • 当你准备不再使用这个指针的时候,你必须调用 Release 方法通知对象,对象内部会将引用计数减1。在调用 Release 方法后不要在使用这个指针(如果你有多个指针指向同一个对象,你可以使用那个没有释放的指针,但是释放后的一定不要再次使用)。

  • 当每一个指向对象的指针都调用 Release 释放后,对象内部的引用计数即为0,则会自动删除自身。

下面是一个过程的简单演示:

释放引用

程序中创建一个对象,并将对象的指针存储到变量中,这个时候的引用计数为1。当程序不在使用该指针的时候,调用Release,则引用计数归0,这个对象会删除自身。指针的内容已经无效,如果这时候使用指针调用对象的任何方法,都可能产生一个致命的错误。

下面是一个过程的简单演示:

多引用

上面的过程中,程序创建一个指针 p,初始时引用计数为 1,接着程序将 p 拷贝了一份,命名为 q。这个时候,程序必须调用 AddRef 方法增加引用计数,完成后,引用计数为2。现在有两个指针同时指向一个对象,假设程序使用 p 完成了某些工作,调用了 Release 函数,则应用计数变为1。这时候 p 指针已经无效。然而这个时候 q 指针仍然有效,当 q 指针完成工作准备不再使用时,再次调用 Release,应用计数归 0,对象删除自身。

你可以能有所疑惑为什么程序需要拷贝 p 指针,一直使用一个指针不行么?这里有两个主要原因:第一种,你可能将指针变量存储到一个数据结构中,例如一个列表。第二种,你可能想要指针用在当前作用域之外,例如,你将一个函数内部创建的指针变量保存到一个全局变量中。

引用计数的优点是你可以在代码的不同阶段共享对象指针,而不需要协调删除该对象的操作顺序。如果不再需要使用该指针,只需要调用 Release 即可,这个对象会在没人使用的时候自动删除。

你可能在吐槽这根本不自动!确实这样,个人感觉 COM 组件只能算是半自动,因为你需要实时调用 Release 和 AddRef 方法来修改组件内部的引用计数,确保其能够正确运作。现代 C 智能指针可以通过重载和析构函数的特性在一定程度上免除这种操作,但 COM 组件并不是 C,所以默认情况下还是需要你去主动通知 COM,帮助其确保内部引用计数的准确性。

2. 案例

这个是打开对话框示例的代码:

HRESULT hr=CoInitializeEx(NULL,COINIT_APARTMENTTHREADED|COINIT_DISABLE_OLE1DDE);
if(SUCCEEDED(hr)){
    IFileOpenDialog *pFileOpen;
    hr=CoCreateInstance(CLSID_FileOpenDialog,NULL,CLSCTX_ALL,IID_IFileOpenDialog,reinterpret_cast<void**>(&pFileOpen));
    if(SUCCEEDED(hr)){
        hr=pFileOpen->Show(NULL);
        if(SUCCEEDED(hr)){
            IShellItem *pItem;
            hr=pFileOpen->GetResult(&pItem);
            if(SUCCEEDED(hr)){
                PWSTR pszFilePath;
                hr=pItem->GetDisplayName(SIGDN_FILESYSPATH,&pszFilePath);
                if(SUCCEEDED(hr)){
                    MessageBox(NULL,pszFilePath,L"File Path",MB_OK);
                    CoTaskMemFree(pszFilePath);
                }
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    CoUninitialize();
}

代码中有两个地方涉及了引用计数。第一个地方在成功创建了 Common Item Dialog 对象。使用完毕后,它必须调用 Release 释放 pFileOpen 指针。

hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
        IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));

if (SUCCEEDED(hr))
{
    // ...
    pFileOpen->Release();
}

第二个地方,当调用 GetResult 方法返回 IShellItem 接口的时候,接口使用完毕后,程序也必须调用 Release 释放 pItem 指针。

hr = pFileOpen->GetResult(&pItem);

if (SUCCEEDED(hr))
{
    // ...
    pItem->Release();
}

上面的案例中,调用 Release 都是在指针即将离开作用域范围的最后一件事情,也就是说,调用 Release 必须在函数调用成功之后,即 SUCCEEDED(hr) 返回真的时候。假设,你在代码中调用 CoCreateInstance 函数失败,则 pFileOpen 指针是无效的,这个时候调用 Release 函数将会是一个错误。

求关注