首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC++

MFC 映射機制深度剖析:訊息對映、句柄對映與效能權衡

     編輯:VC++

前言:MFC 核心基礎設施的兩大支柱

MFC 為了在 C++ 靜態型別系統上構建動態的視窗物件模型,引入了兩大映射機制:

  1. 條件查找映射:訊息對映、命令對映、OLE 介面查詢 —— 實現「虛擬函數表」外的動態分發。
  2. 句柄映射:HWND ↔ CWnd*、HGDIOBJ ↔ CGdiObject* 等 —— 確保同一執行緒內「一個句柄對應一個物件」。

本文剖析 MFC 原始碼關鍵片段,揭示其運作原理、快取優化與多執行緒陷阱。

一、條件查找映射:訊息對映的尋找之旅

1. 訊息對映表結構

struct AFX_MSGMAP
{
    const AFX_MSGMAP* pBaseMap;           // 基類對映表指標
    const AFX_MSGMAP_ENTRY* lpEntries;    // 對映項目陣列
    // ... (DLL 版本使用 pfnGetBaseMap 函數指標)
};

struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;   // 訊息 ID (WM_XXX)
    UINT nCode;      // 控制項通知碼 (BN_CLICKED 等)
    UINT nID;        // 控制項 ID 範圍起始
    UINT nLastID;    // 控制項 ID 範圍結束
    UINT nSig;       // 簽名編碼 (參數類型、回傳類型)
    AFX_PMSG pfn;    // 處理函數指標
};

2. 查找演算法:從衍生類遞迴至基類

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) goto Dispatch; // 找到處理函數
        return FALSE; // 快記錄為「無對映」
    }
    
    // ---- 慢速路徑:遍歷對映表鏈 ----
    pMsgCache->nMsg = message;
    pMsgCache->pMessageMap = pMessageMap;
    
    for (; pMessageMap; pMessageMap = pMessageMap->pBaseMap) // 或 pfnGetBaseMap()
    {
        // 防呆:避免循環繼承 BEGIN_MESSAGE_MAP(CMyWnd, CMyWnd)
        ASSERT(pMessageMap != pMessageMap->pBaseMap);
        
        if (message < 0xC000) // 標準視窗訊息
        {
            if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)
            {
                pMsgCache->lpEntry = lpEntry;
                AfxUnlockGlobals(CRIT_WINMSGCACHE);
                goto Dispatch;
            }
        }
        else // 已註冊訊息 (RegisterWindowMessage)
        {
            for (lpEntry = pMessageMap->lpEntries; 
                 (lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL; 
                 lpEntry++)
            {
                UINT* pnID = (UINT*)(lpEntry->nSig);
                if (*pnID == message)
                {
                    pMsgCache->lpEntry = lpEntry;
                    AfxUnlockGlobals(CRIT_WINMSGCACHE);
                    goto DispatchRegistered;
                }
            }
        }
    }
    
    // 完全找不到
    pMsgCache->lpEntry = NULL;
    AfxUnlockGlobals(CRIT_WINMSGCACHE);
    return FALSE;

Dispatch:
    // 依簽名解碼參數並呼叫 pfn ...
}

3. 關鍵觀察

特性說明
查找策略線性掃描 (非雜湊表),依訊息 ID 比對
繼承支援鏈式遍歷基類對映表,衍生類優先
快取機制單一全域雜湊表 _afxMsgCache,以 (MapPtr ^ MsgID) % Size 索引
執行緒安全全域關鍵區 CRIT_WINMSGCACHE 保護快取讀寫
優化建議常用訊息對映項目前置;避免大量動態註冊訊息

4. 為何不用虛擬函數?

  • 空間效率: 每個視窗類別僅儲存「有對映」的訊息項目,而非完整虛擬函數表 (vtable) 槽位。
  • 彈性: 衍生類可任意增減對映項目,無需重寫查找邏輯。
  • 代價: 執行期線性查找 (經快取緩解) + 全域鎖爭用。

二、句柄映射:HWND 與 CWnd* 的雙向綁定

1. 核心映射表

每個執行緒擁有獨立映射表 (TLS 儲存):

  • afxMapHWND():永久映射 (CWnd::FromHandlePermanent)。
  • afxMapHWND() 臨時映射 (CWnd::FromHandle) —— GetDlgItem、SelectObject 回傳的臨時句柄包裝。

2. 關鍵函數實作

// 取得父框架視窗
CFrameWnd* CWnd::GetParentFrame() const
{
    if (GetSafeHwnd() == NULL) return NULL;
    ASSERT_VALID(this);
    
    CWnd* pParentWnd = GetParent(); // 向上遍歷父視窗
    while (pParentWnd != NULL)
    {
        if (pParentWnd->IsFrameWnd()) // 虛擬函數判斷
            return (CFrameWnd*)pParentWnd;
        pParentWnd = pParentWnd->GetParent();
    }
    return NULL;
}

inline CWnd* CWnd::GetParent() const
{
    ASSERT(::IsWindow(m_hWnd));
    return CWnd::FromHandle(::GetParent(m_hWnd)); // API → 句柄映射 → 物件
}

3. 訊息分發入口:AfxWndProc

LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    if (nMsg == WM_QUERYAFXWNDPROC) return 1; // 識別標記
    
    // 關鍵:從永久映射取得 CWnd*
    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
    ASSERT(pWnd != NULL);
    ASSERT(pWnd->m_hWnd == hWnd);
    
    if (pWnd == NULL || pWnd->m_hWnd != hWnd)
        return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
    
    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); // 進入訊息對映
}
效能代價: 每個訊息到達皆需執行 FromHandlePermanent 查找映射表 (TLS 讀取 + 雜湊查找)。高頻訊息 (WM_MOUSEMOVE, WM_TIMER) 累積顯著。

4. 多執行緒陷阱:TLS 隔離導致物件不一致

同一 HWND 在不同執行緒查找可能得到不同 CWnd* (或 NULL),因映射表儲存於 Thread Local Storage (TLS)。

  • 執行緒 A 建立視窗 → 映射表記錄 HWND → CWnd*。
  • 執行緒 B 呼叫 CWnd::FromHandle(hWnd) → 該執行緒映射表無紀錄 → 產生臨時物件 或 NULL。
  • 結果:跨執行緒操作同一視窗物件導致狀態不同步、Assert 失敗、資源洩漏。

解決方案: 參考 KB Q147578 —— UI 物件專屬於建立執行緒,跨執行緒操作必須 PostMessage/SendMessage 封送。

三、總結與架構啟示

映射類型優點缺點適用場景
條件查找映射動態分發、支援繼承覆寫、節省 vtable 空間查找開銷、全域鎖爭用、除錯困難訊息/命令/介面分發
句柄映射封裝 C API、確保 1:1 綁定、支援臨時包裝TLS 隔離、每訊息查找開銷、多執行緒陷阱HWND/HGDI/菜單 物件化

現代框架的借鑑

  • WPF/WinUI: 視覺樹 + 邏輯樹 + 路由事件 (Bubbling/Tunneling) 替代訊息對映;DependencyObject 內建查找機制。
  • React/Vue: 虛擬 DOM + 事件代理 + 合成事件,無原生句柄映射需求。
  • Rust GUI (egui, iced): 即時模式 (Immediate Mode) 完全避開持久物件映射。

結語

MFC 的雙重映射機制是「C++ 封裝 C API」歷史妥協下的精妙產物:以空間換靈活性、以快取換速度、以 TLS 換執行緒隔離。理解其運作細節,不僅能避開多執行緒坑、優化高頻訊息路徑,更能啟發現代框架設計中「物件身分識別與分發機制」的權衡思考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved