从 VC7 的 CHtmlView 不能正常退出谈 CComPtr 使用中的一个误区
一、错误再现
在 VC7 中新建一个 MDI 的 MFC Application,命名为MyHtml, 选择使用 CHtmlView。
建立两个 html 文件:
home.htm
test.htm
修改 CMyHtmlView 的 OnInitialUpdate()
void CMyHtmlView::OnInitialUpdate()
{
CHtmlView::OnInitialUpdate();
Navigate2(_T(" http://./home.htm"));
}
编译并运行这个程序,在子窗口打开后将其关闭。你会发现浏览器控件还在运行。
二、错误分析
在 VC7 中,MFC 在很大程度上使用了 ATL,CHtmlView 也不例外,在 CHtmlView 中,访问 COM 指针的代码被修改为使用 ATL 的 CComPtr。CComPtr 是一个对 COM 指针进行包装的 ATL 模版,它实现了引用时自动 AddRef 和退出时自动 Release 这些以前很烦琐的操作。而由其发展出来的 CComQIPtr 则更将 QueryInterface 包装成 "=" 运算符,更加方便使用。对于这两个模版的详细介绍,不在本文的探讨范围,我只能假设您已经基本了解并已经用过这两个模版。
我们再来看看 VC7 的 CHtmlView 对 CComPtr 的使用方法。在函数 OnFilePrint 中,CHtmlView 的代码是这样的:
void CHtmlView::OnFilePrint()
{
// get the HTMLDocument
if (m_pBrowserApp != NULL)
{
CComPtr spDisp = GetHtmlDocument();
if (spDisp != NULL)
{
// the control will handle all printing UI
CComQIPtr spTarget = spDisp;
if (spTarget != NULL)
spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
}
}
}
在我所标记的一行中我们看到这样的代码:
CComPtr spDisp = GetHtmlDocument();
而 GetHtmlDocument 的实现是什么样的呢?我们再来看看:
LPDISPATCH CHtmlView::GetHtmlDocument()
const
{
ASSERT(m_pBrowserApp != NULL);
LPDISPATCH result;
m_pBrowserApp->get_Document(&result);
return result;
}
可以知道,GetHtmlDocument 返回的是 get_Document 所输出的一个接口指针,而我们知道,对于 COM 指针的一个使用原则是输出参数时进行引用计数,也就是说我们所获得的这个 result 在 get_Document 内部已经对其进行了 AddRef 调用,函数的调用者在不再需要这个指针的时候必须自行对指针进行 Release。
继续,我们再回头看 OnFilePrint 的代码,在代码中使用了 CComPtr 重载过的 "=" 运算符将函数的返回指针赋值给 spDisp。我们已经知道 CComPtr 在函数退出的时候会自动对其所包装的指针进行 Release,一切看起来都是正常而且天体无缝的。
那么到底错在哪里呢?恰恰就错在了这个 "=" 上面。
依照 COM 指针的引用时计数的原则,CComPtr 在实现的时候实现了自动化的引用计数。即在任何 "=" 操作的时候 AddRef,而在无效时 Release。我们来看看 "=" 运算符的具体实现代码是什么样的:
ATLINLINE ATLAPI_(IUnknown*) AtlComPtrAssign(IUnknown** pp, IUnknown* lp)
{
if (lp != NULL)
lp->AddRef();
if (*pp)
(*pp)->Release();
*pp = lp;
return lp;
}
从这段代码可以知道,CComPtr 在拿到指针后,并不是直接将其保存到自己的指针里面,而是先对拿到的指针进行 AddRef,保证引用计数,而后才执行 *pp = lp。
这样以来,我们将三部分代码合并起来就成了这样:
void CHtmlView::OnFilePrint()
{
LPDISPATCH result;
// 函数 GetHtmlDocument
m_pBrowserApp->get_Document(&result);
// 函数 GetHtmlDocument
IDispatch* spDisp;
result->AddRef();
// CComPtr 自动完成
spDisp = result;
// CComPtr 自动完成
.......
spDisp->Release();
// CComPtr 自动完成
}
能够看出其中的问题吗?对了,result 并没有被释放。问题出在函数输出的并不是一个引用计数完整的 COM 指针,而 CComPtr 并不知道,从而导致了这个指针最终被丢失。而 COM 对象也因为引用计数并没有回归为零而不敢清除自己,最终导致了 CHtmlView 不能正常退出。
三、修改
通过对上面代码的分析,我们已经清楚了解了 CHtmlView 错误的原因,下面我们就来试图对 CHtmlView 进行修正。
1.将 PROGRAM FILES/MICROSOFT VISUAL STUDIO .NET/Vc7/atlmfc/src/mfc 目录中的 viewhtml.cpp 复制到你自己的项目目录,并将其加入到自己的项目中。
2.打开 viewhtml.cpp, 寻找 GetHtmlDocument。
3.将所有的直接将 GetHtmlDocument 函数返回赋值给 CComPtr 指针的语句修改为使用 CComPtr 的 Attach。以 OnFilePrint 为例,代码将修改为下面的样子:
void CHtmlView::OnFilePrint()
{
// get the HTMLDocument
if (m_pBrowserApp != NULL)
{
CComPtr spDisp;
spDisp.Attach(GetHtmlDocument());
if (spDisp != NULL)
{
// the control will handle all printing UI
CComQIPtr spTarget = spDisp;
if (spTarget != NULL)
spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
}
}
}
重新编译你的程序,再用最开始我提到的 html 进行测试,你会发现一切都正常了。看起来麻烦一些,但是是正确的。
四、结论
通过上面分析纠错,我们可以知道,CComPtr 并不是一把万能钥匙,而对 COM 指针的使用也远没有因为 ATL 的出现而变得通俗起来。如果具体到这个例子,我们可以得到一个结论:
任何时候不要将函数的返回指针赋值给一个 CComPtr。