從 VC7 的 CHtmlView 不能正常退出談 CComPtr 使用中的一個誤區

從 VC7 的 CHtmlView 不能正常退出談 CComPtr 使用中的一個誤區
響馬<[email protected]>
 
一、錯誤再現
 
在 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。
文章出處:飛諾網(www.firnow.com):http://dev.firnow.com/course/3_program/c++/cppjs/20090521/167525.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章