在項目開發過程,我會經常檢查的一些問題,導緻程式崩潰。隻是前一段時間,測試組的回報現象,當處理用于尋呼機的特定功能,經過約半小時進行,該計劃将導緻崩潰,反複試驗幾次後,均高于現象。對崩潰日志的初步分析。我發現。盡管每次崩潰的地方一緻。但通過閱讀源代碼,我發現那些地方通常是不會出現故障的,難道讀代碼不夠細緻,我仔細緻細地分析了一遍發生崩潰的上下文,都是很正常的調用情況。難道又出現了其他子產品的幹擾,造成主線程記憶體錯亂了,常常查找崩潰的人都知道。假設出現了這種情況,那查找起來無異于登天,盡管有豐富的定位崩潰的經驗,但每次遇到這種問題,你的經驗往往僅僅能起到一丁點的輔助作用。
就在我一籌莫展的時候。測試人員邊進行拷機,邊随口說了一聲,“這個窗體怎麼我剛移動了位置,如今又回到了原來位置?”。每當山窮水複疑無路的時候。不論什麼一絲的細節都可能成為突破的關鍵,我說。“可能新彈的窗體又把原來的窗體置位了吧。”。測試人員便應聲到,“這樣互動好像不是非常友好吧!”,而此時我關心的并非互動的問題,而是這個崩潰何解。便任意答應了一下,說“那我去看一下代碼。”
打開源代碼。簡單幾句代碼出如今了眼前:
bool XXX::YYY( WPARAM wParam, LPARAM lParam, bool& bHandled )
{
HWND hWnd = CConfCtrlLogic::Instance()->GetApplyChimeRspWnd();
if ( ::IsWindow( hWnd ) && ::IsWindowVisible( hWnd ) )
{
SendMessage( hWnd, WM_CLOSE, 0, 0 );
}
CStdString strInfo = wParam ? STRING_JOIN_DISCUSS_SUCC : STRING_JOIN_DISCUSS_FAIL;
CMessageBoxDlg dlg( IDR_XML_MSG_NOTIFY_DLG, 255, false );
dlg.EnbaleAutoClose( FALSE );
dlg.SetInfo( strInfo, STRING_TIP, g_pMainLogic->GetMainHwnd(), ID_OK );
dlg.Create( g_pMainLogic->GetMainHwnd(), STRING_TIP, UI_WNDSTYLE_BOX, WS_EX_TOOLWINDOW );
CConfCtrlLogic::Instance()->SetApplyChimeRspWnd( dlg.GetHWND() );
dlg.CenterWindow();
dlg.ShowModal();
bHandled = true;
return true;
}
這個函數便是彈出框起始的地方,拷機會定時有消息産生。也就是這個函數會被定時調用。該函數的大緻意思是:
1:假設有彈出框而且彈出框是顯示的。就發個 WM_CLOSE 消息,關掉它。
2:建立一個彈出框,并調用ShowModal 顯示。
看似簡單而且無差錯的邏輯,卻引起了我的警覺,我下意識地點開了 ShowModal 的源代碼,由于 ShowModal 在這個版本号産生了無數個問題,糾其原因。就是 DUI 的 ShowModal 也接管了消息循環。而且做了簡單的邏輯處理,代碼例如以下:
UINT CWindowWnd::ShowModal()
{
ASSERT( ::IsWindow(m_hWnd) );
UINT nRet = 0;
HWND hWndParent = GetWindowOwner( m_hWnd );
::ShowWindow( m_hWnd, SW_SHOWNORMAL );
::EnableWindow( hWndParent, FALSE );
MSG msg = { 0 };
while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) )
{
if( WM_CLOSE == msg.message && msg.hwnd == m_hWnd )
{
nRet = msg.wParam;
::EnableWindow( hWndParent, TRUE );
::SetFocus( hWndParent );
}
if( !CPaintManagerUI::TranslateMessage(&msg) )
{
::TranslateMessage( &msg );
::DispatchMessage( &msg );
}
if( WM_QUIT == msg.message )
{
break;
}
}
::EnableWindow( hWndParent, TRUE );
::SetFocus( hWndParent );
if( WM_QUIT == msg.message )
{
::PostQuitMessage( msg.wParam );
}
return nRet;
}
通過代碼能夠看到,消息循環會推斷是不是窗體,而且取出一個消息,然後處理掉,當接收到 WM_CLOSE 消息後,在 WM_CLOSE 消息中我們的通常處理都會是關掉窗體,然後導緻 IsWindow 判定失敗。退出消息循環。也算是很正常的邏輯。
但結合上段代碼的調用就會發現。這當中是有問題的。
有幾點要明白:
1. XXX::YYY 被調用的時候,肯定是在一個消息循環裡;
2. 當 SendMessage 傳入 WM_CLOSE 的時候,會直接去調用視窗過程,并處理WM_CLOSE分支的業務,通常是銷毀視窗,此時也還在XXX::YYY 被調用時的消息循環裡;
3. 但 SendMessage 傳回。接着調用 ShowModal 時,依舊是在前兩步同樣的消息循環裡,一直沒有出消息循環。
問題就這樣産生了。ShowModal 會建立一個消息循環。且 ShowModal 不傳回。堆棧繼續向下漲,當 XXX::YYY 相關的消息再産生的時候,就會在上一個 ShowModal 産生的消息循環裡處理 XXX::YYY ,然後繼續 SendMessage and ShowModal 再建立,周而複始。卻一直沒有退出過一個消息循環,當然也就沒有傳回過函數。我們知道,函數僅僅調用不傳回,當然堆棧會一直向下漲,最後造成堆棧溢出。
回顧一下。測試描寫叙述的問題是,隔一定時間,程式必崩。這也符合了崩潰場景,由于拷機是以固定時間來産生這個消息。而調用環境同樣,那麼棧增長速度必定同樣,棧大小固定,那麼隔固定時間後,也必定會引起棧溢出,至此,這個崩潰被定位,那解決方法就非常easy了。我們採用了一位同僚的非常好的建議。像這樣的不須要使用者确定的通知型消息,根本不用模态框來完畢,是以直接使用了 Pop 框通知一下。最後也由那位同僚(weilaitao)行了代碼的改動。
堆棧溢出的問題有時是很頭疼的。而像這樣的由特殊業堆棧溢出的服務間接原因是罕見,并檢查珍惜。