从 VC7 的 CHtmlView 不能正常退出谈 CComPtr 使用中的一个误区
一、错误再现
在 VC7 中新建一个 MDI 的 MFC Application,命名为MyHtml, 选择使用 CHtmlView。
建立两个 html 文件:
home.htm
<head>
<frameset rows="*,30">
<frame src="test.htm">
<frame src="about:blank">
</frameset>
</html>
test.htm
<html>
<head>
<script language="JavaScript"><!--
function FreshNew()
{
window.alert("I'am here.");
setTimeout('FreshNew();',2000);
}
setTimeout('FreshNew();',2000);
// --></script>
</head>
</html>
修改 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<IDispatch> spDisp = GetHtmlDocument();
if (spDisp != NULL)
{
// the control will handle all printing UI
CComQIPtr<IOleCommandTarget> spTarget = spDisp;
if (spTarget != NULL)
spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
}
}
}
在我所标记的一行中我们看到这样的代码:
CComPtr<IDispatch> 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<IDispatch> spDisp;
spDisp.Attach(GetHtmlDocument());
if (spDisp != NULL)
{
// the control will handle all printing UI
CComQIPtr<IOleCommandTarget> spTarget = spDisp;
if (spTarget != NULL)
spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
}
}
}
重新编译你的程序,再用最开始我提到的 html 进行测试,你会发现一切都正常了。看起来麻烦一些,但是是正确的。
四、结论
通过上面分析纠错,我们可以知道,CComPtr 并不是一把万能钥匙,而对 COM 指针的使用也远没有因为 ATL 的出现而变得通俗起来。如果具体到这个例子,我们可以得到一个结论:
任何时候不要将函数的返回指针赋值给一个 CComPtr。