MFC 大量使用映射表實現訊息分派、命令路由、OLE 介面查詢等核心功能。理解其內部運作有助於撰寫高效能程式碼並進行深度除錯。本文分析兩大核心映射:條件查找映射 (訊息/命令/介面) 與 控制代碼映射 (HWND/HGDI 物件對應)。
QueryInterface) 等。CWnd::OnWndMsg 為例)BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
const AFX_MSGMAP* pMessageMap = GetMessageMap();
UINT iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax - 1);
AfxLockGlobals(CRIT_WINMSGCACHE);
AFX_MSG_CACHE* pMsgCache = &_afxMsgCache[iHash];
// 快取命中檢查
if (message == pMsgCache->nMsg && pMessageMap == pMsgCache->pMessageMap)
{
const AFX_MSGMAP_ENTRY* lpEntry = pMsgCache->lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
if (lpEntry == NULL) return FALSE;
// 依訊息類型跳轉分派
if (message < 0xC000) goto LDispatch;
else goto LDispatchRegistered;
}
else
{
// 快取未命中,遍歷映射鏈
pMsgCache->nMsg = message;
pMsgCache->pMessageMap = pMessageMap;
#ifdef _AFXDLL
for (; pMessageMap->pfnGetBaseMap != NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)())
#else
for (; pMessageMap != NULL; pMessageMap = pMessageMap->pBaseMap)
#endif
{
// 防止循環參考
#ifdef _AFXDLL
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
#else
ASSERT(pMessageMap != pMessageMap->pBaseMap);
#endif
if (message < 0xC000) // 標準視窗訊息
{
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto LDispatch;
}
}
else // 已註冊訊息
{
lpEntry = pMessageMap->lpEntries;
while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
{
UINT* pnID = (UINT*)(lpEntry->nSig);
if (*pnID == message)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto LDispatchRegistered;
}
lpEntry++;
}
}
}
pMsgCache->lpEntry = NULL;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
return FALSE;
}
LDispatch:
// 實際分派處理...
}
_afxMsgCache,命中時直接取得處理函式入口,避免重複遍歷。iHash,實際上仍是線性搜尋映射表項目。因此:| 優點 | 缺點 |
|---|---|
| 衍生類輕鬆擴充/覆寫映射,無需重寫查找邏輯 | 理解難度較高,除錯需深入原始碼 |
| 避免龐大虛函數表開銷 | 查找效能不如直接虛函數呼叫 |
支援動態介面查詢 (如 IDispatch) | 多執行緒環境下需注意鎖定開銷 |
將 Windows 控制代碼 (HWND, HDC, HMENU 等) 一對一對應至 MFC 物件指標 (CWnd*, CDC*, CMenu*),確保同一執行緒內同一控制代碼對應唯一物件。
CWnd::FromHandlePermanent 查找永久關聯物件。GetDlgItem, SelectObject 等 API 回傳的暫時控制代碼,建立臨時物件 (CTempWnd 等),閒置時自動清理。CWnd::GetParentFrameCFrameWnd* CWnd::GetParentFrame() const
{
if (GetSafeHwnd() == NULL) return NULL;
ASSERT_VALID(this);
CWnd* pParentWnd = GetParent(); // 呼叫 API GetParent
while (pParentWnd != NULL)
{
if (pParentWnd->IsFrameWnd())
return (CFrameWnd*)pParentWnd;
pParentWnd = pParentWnd->GetParent();
}
return NULL;
}
_AFXWIN_INLINE CWnd* CWnd::GetParent() const
{
ASSERT(::IsWindow(m_hWnd));
return CWnd::FromHandle(::GetParent(m_hWnd)); // 映射查找
}
流程:API 取得父視窗控制代碼 → FromHandle 在執行緒映射表查找對應 CWnd* → 呼叫虛函數 IsFrameWnd 判斷類型。
每個訊息到達時,AfxWndProc 皆需呼叫 CWnd::FromHandlePermanent 遍歷永久映射表:
LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
if (nMsg == WM_QUERYAFXWNDPROC) return 1;
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
ASSERT(pWnd != NULL && pWnd->m_hWnd == hWnd);
if (!pWnd || pWnd->m_hWnd != hWnd) return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
此映射查找是 MFC 應用程式效能損耗來源之一。
因映射表儲於 TLS,同一 HWND 在不同執行緒可能對應不同物件指標。跨執行緒操作視窗物件需謹慎,參考微軟知識庫 Q147578。
MFC 的雙重映射機制在開發便利性與執行效能間取得平衡。開發者應理解其運作原理,於效能敏感路徑考慮直接使用 API 或快取查找結果,並在多執行緒場景下妥善處理物件關聯。