本文講解SendMessage、PostMessage兩個函數的實現原理,分為三個步驟進行講解,分別適合初級、中級、高級程序員進行理解,三個步驟分別為:
1、SendMessage、PostMessage的運行機制。
2、SendMessage、PostMessage的運行內幕。
3、SendMessage、PostMessage的內部實現。
注:理解這篇文章之前,必須先了解Windows的消息循環機制。
?
1、SendMessage、PostMessage的運行機制
我們先來看最簡單的。
SendMessage可以理解為,SendMessage函數發送消息,等待消息處理完成后,SendMessage才返回。稍微深入一點,是等待窗口處理函數返回后,SendMessage就返回了。
PostMessage可以理解為,PostMessage函數發送消息,不等待消息處理完成,立刻返回。稍微深入一點,PostMessage只管發送消息,消息有沒有被送到則并不關心,只要發送了消息,便立刻返回。
對于寫一般Windows程序的程序員來說,能夠這樣理解也就足夠了。但SendMessage、PostMessage真的是一個發送消息等待、一個發送消息不等待嗎?具體細節,下面第2點將會講到。
?
2、SendMessage、PostMessage的運行內幕
在寫一般Windows程序時,如上第1點講到的足以應付,其實我們可以看看MSDN來確定SendMessage、PostMessage的運行內幕。
在MSDN中,SendMessage解釋如為:The SendMessage function sends the specified message to a window or windows. It calls the window procedure for the specified window and does not return until the window procedure has processed the message.
翻譯成中文為:SendMessage函數將指定的消息發到窗口。它調用特定窗口的窗口處理函數,并且不會立即返回,直到窗口處理函數處理了這個消息。
再看看PostMessage的解釋:The PostMessage function places (posts) a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message.
??? 翻譯成中文為:PostMessage函數將一個消息放入與創建這個窗口的消息隊列相關的線程中,并立刻返回不等待線程處理消息。
仔細看完MSDN解釋,我們了解到,SendMessage的確是發送消息,然后等待處理完成返回,但發送消息的方法為直接調用消息處理函數(即WndProc函數),按照函數調用規則,肯定會等消息處理函數返回之后,SendMessage才返回。而PostMessage卻沒有發送消息,PostMessage是將消息放入消息隊列中,然后立刻返回,至于消息何時被處理,PostMessage完全不知道,此時只有消息循環知道被PostMessage的消息何時被處理了。
至此我們撥開了一層疑云,原來SendMessage只是調用我們的消息處理函數,PostMessage只是將消息放到消息隊列中。下一節將會更深入這兩個函數,看看Microsoft究竟是如何實現這兩個函數的。
?
3、SendMessage、PostMessage的內部實現
Windows內部運行原理、機制往往是我們感興趣的東西,而這些東西又沒有被文檔化,所以我們只能使用Microsoft提供的工具自己研究了。
首先,在基本Win32工程代碼中,我們可以直接看到消息處理函數、消息循環,所以建立一個基本Win32工程(本篇文章使用VS2005),為了看到更多信息,我們需要進行設置,讓VS2005載入Microsoft的Symbol(pdb)文件[1]。為了方便,去除了一些多余的代碼,加入了兩個菜單,ID分別為:IDM_SENDMESSAGE、IDM_POSTMESSAGE。如下列出經過簡化后的必要的代碼。
消息循環:
Ln000:while (GetMessage(&msg, NULL, 0, 0))
Ln001:{
Ln002:??? TranslateMessage(&msg);
Ln003:??? DispatchMessage(&msg);
Ln004:}
?
消息處理函數:
Ln100:LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Ln101:{
Ln102:??? int wmId, wmEvent;
Ln103:??? switch (message)
Ln104:??? {
Ln105:??? case WM_COMMAND:
Ln106:??????? wmId = LOWORD(wParam);
Ln107:??????? wmEvent = HIWORD(wParam);
Ln108:??? ????switch (wmId)
Ln109:??????? {
Ln110:??????? case IDM_EXIT:
Ln111:??????? ????DestroyWindow(hWnd);
Ln112:??????????? break;
Ln113:??????? case IDM_SENDMESSAGE:
Ln114:??????????? SendMessage(hWnd, WM_SENDMESSAGE, 0, 0);
Ln115:??????????? break;
Ln116:?? ?????case IDM_POSTMESSAGE:
Ln117:??????????? PostMessage(hWnd, WM_POSTMESSAGE, 0, 0);
Ln118:??????????? break;
Ln119:??????? default:
Ln120:??????????? return DefWindowProc(hWnd, message, wParam, lParam);
Ln121:?? ?????}
Ln122:??????? break;
Ln123:
Ln124: ???case WM_SENDMESSAGE:
Ln125:??????? MessageBox(hWnd, L"SendMessage", L"Prompt", MB_OK);
Ln126:??????? break;
Ln127:
Ln128:??? case WM_POSTMESSAGE:
Ln129:??????? MessageBox(hWnd, L"PostMessage", L"Prompt", MB_OK);
Ln130:??????? break;
Ln131:
Ln132:??? case WM_DESTROY:
Ln133:??????? PostQuitMessage(0);
Ln134:
Ln135:??? default:
Ln136:??????? return DefWindowProc(hWnd, message, wParam, lParam);
Ln137:??? }
Ln138:??? return 0;
Ln139:}
下面一步步分析這兩個函數的內部情況,先討論 SendMessage。
第一步,在DispatchMessage(Ln003)函數處下個斷點,F5進行調試,當程序運行到斷點后,查看 CallStack 窗口,可得如下結果:
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)? Line 49?? C++
#002:MyProj.exe!__tmainCRTStartup()? Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()? Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()? + 0x23 bytes?
我們可以看到,進程先調用 kernel32.dll 中的 BaseProcessStart 函數,然后調用的 Startup Code 的函數 wWinMainCRTStartup,然后調用 _tmainCRTStartup 函數,最終調用我們的 wWinMain 函數,我們的程序就運行起來了。
?
第二步,去除第一步下的斷點,在 WndProc(Ln101) 函數入口處下個斷點,F5 繼續運行,運行到新下的斷點處,查看 CallStack 窗口,可得如下結果:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000101, unsigned int wParam=0x00000074, long lParam=0xc03f0001)? Line 122??? C++
#007:user32.dll!_InternalCallWinProc@20()? + 0x28 bytes???
#006:user32.dll!_UserCallWinProcCheckWow@32()? + 0xb7 bytes???
#005:user32.dll!_DispatchMessageWorker@8()? + 0xdc bytes??
#004:user32.dll!_DispatchMessageW@4()? + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001)? Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup()? Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()? Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()? + 0x23 bytes?
??? #000~#003 跟第一步相同,不再解釋。在 #004、#005,可以看到,函數運行到 DispatchMessage 的內部了,DispatchMessageW、DispatchMessageWorker 是 user32.dll 中到處的函數,而且函數前部字符串相等,在此猜想應該是 DispatchMessage 的內部處理。#008 為我們消息處理函數,所以推想而知,#006、#007 是為了調用我們的消息處理函數而準備的代碼。
?
第三步,去除第二步下的斷點,在Ln003、Ln114、Ln115、Ln125 處分別下一個斷點,在菜單中選擇對應項,使程序運行至 Ln114,F10下一步,可以看到并沒有運行到 break(Ln115),直接跳到了 Ln125 處,由此可知目前 SendMessage 已經在等待了,查看 CallStack 窗口,可得如下結果:
#013:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000500, unsigned int wParam=0x00000000, long lParam=0x00000000)? Line 147??? C++
#012:user32.dll!_InternalCallWinProc@20()? + 0x28 bytes???
#011:user32.dll!_UserCallWinProcCheckWow@32()? + 0xb7 bytes???
#010:user32.dll!_SendMessageWorker@20()? + 0xc8 bytes?
#009:user32.dll!_SendMessageW@16()? + 0x49 bytes??
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000)? Line 136 + 0x15 bytes?? C++
#007:user32.dll!_InternalCallWinProc@20()? + 0x28 bytes???
#006:user32.dll!_UserCallWinProcCheckWow@32()? + 0xb7 bytes???
#005:user32.dll!_DispatchMessageWorker@8()? + 0xdc bytes??
#004:user32.dll!_DispatchMessageW@4()? + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001)? Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup()? Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()? Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()? + 0x23 bytes?
#000~#008 跟上面的相同,不再解釋。在 #009、#010,可以看到,函數調用到 SendMessage 內部了,在此猜想應該是 SendMessage 的內部處理。#011、#012 跟第二步中的 #006、#007 一樣,在第二部中,這兩個函數是為了調用消息處理函數而準備的代碼,#013 也是我們的消息處理函數,所以此兩行代碼的功能相等。
至此,我們證明了 SendMessage 的確是直接調用消息處理函數的,在消息處理函數返回前,SendMessage 等待。在所有的操作中,Ln003 斷點沒有去到,證明 SendMessage 不會將消息放入消息隊列中(在 PostMessage 分析中,此斷點將會跑到,接下來講述)。
?
第四步,F5繼續運行,此時彈出對話框,點擊對話框中的確定后,運行到斷點 Ln115 處。查看 CallStack 窗口,可得如下結果:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000)? Line 137??? C++
#007:user32.dll!_InternalCallWinProc@20()? + 0x28 bytes???
#006:user32.dll!_UserCallWinProcCheckWow@32()? + 0xb7 bytes???
#005:user32.dll!_DispatchMessageWorker@8()? + 0xdc bytes??
#004:user32.dll!_DispatchMessageW@4()? + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)? Line 49 + 0xc bytes?? C++
#002:MyProj.exe!__tmainCRTStartup()? Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()? Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()? + 0x23 bytes?
#000~008 跟第二步的完全相同,此時 SendMessage 也已經返回,所調用的堆棧也清空了。
至此,我們徹底撥開了 SendMessage 的疑云,了解了 SendMessage 函數的運行機制,綜述為,SendMessage 內部調用 SendMessageW、SendMessageWorker 函數做內部處理,然后調用 UserCallWinProcCheckWow、InternalCallWinProc 來調用我們代碼中的消息處理函數,消息處理函數完成之后,SendMessage 函數便返回了。
?
SendMessage 討論完之后,現在討論 PostMessage,將上面的所有斷點刪除,關閉調試。
第一步,在DispatchMessage(Ln003)函數處下個斷點,F5進行調試,此處結果跟 SendMessage 一樣,不再說明。
第二步,去除第一步下的斷點,在 WndProc(Ln101) 函數入口處下個斷點,F5 繼續運行,此處結果跟 SendMessage 一樣,不再說明。
第三步,去除第二步下的斷點,在 Ln003、Ln117、Ln118、Ln129 處分別下一個斷點,在菜單中選擇對應項,使程序運行至 Ln117,F10 下一步,可以看到已經運行到 break,PostMessage 函數返回了,此時 CallStack 沒有變化。
第四步,F5 繼續運行,此時程序運行到 Ln003,CallStack 跟第一步剛起來時一樣。
第五步,F5 繼續運行(由于有多個消息,可能要按多次),讓程序運行到 Ln129,此時 CallStack 跟第二步相同,為了方便說明,再次列舉如下:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00070874, unsigned int message=0x00000501, unsigned int wParam=0x00000000, long lParam=0x00000000)? Line 151??? C++
#007:user32.dll!_InternalCallWinProc@20()? + 0x28 bytes???
#006:user32.dll!_UserCallWinProcCheckWow@32()? + 0xb7 bytes???
#005:user32.dll!_DispatchMessageWorker@8()? + 0xdc bytes??
#004:user32.dll!_DispatchMessageW@4()? + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001)? Line 49 + 0xc bytes?? C++
#002:MyProj.exe!__tmainCRTStartup()? Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup()? Line 414 C
#000:kernel32.dll!_BaseProcessStart@4()? + 0x23 bytes?
由此可以看到,此調用是從消息循環中調用而來,DispatchMessageW、DispatchMessageWorker 是 DispatchMessage 的內部處理,UserCallWinProcCheckWow、InternalCallWinProc是為了調用我們的消息處理函數而準備的代碼。
至此,我們再次徹底撥開了 PostMessage 的疑云,了解了 PostMessage 函數的運行機制,綜述為,PostMessage 將消息放入消息隊列中,自己立刻返回,消息循環中的 GetMessage(PeekMessage 也可,本例中為演示)處理到我們發的消息之后,便按照普通消息處理方法進行處理。