在项目开发过程中,我会经常查一些引起程序崩溃的问题。就在前段时间,测试组反馈了一个现象,当对某一个功能进行拷机的过程中,大约进行了半个小时之后,程序就会引起崩溃,经过数次的重复测试,均出现了以上现象。经过对崩溃日志的初步分析,我发现,虽然每次崩溃的地方一致,但通过阅读源码,我发现那些地方一般是不会出现问题的,难道读代码不够仔细,我仔仔细细地分析了一遍发生崩溃的上下文,都是非常正常的调用情况。难道又出现了其它模块的干扰,造成主线程内存错乱了,经常查找崩溃的人都知道,如果出现了这种情况,那查找起来无异于登天,虽然有丰富的定位崩溃的经验,但每次遇到这样的问题,你的经验往往只能起到一丁点的辅助作用。
就在我一筹莫展的时候,测试人员边进行拷机,边随口说了一声,“这个窗口怎么我刚移动了位置,现在又回到了原来位置?”,每当山穷水复疑无路的时候,任何一丝的细节都可能成为突破的关键,我说,“可能新弹的窗口又把原来的窗口置位了吧。”,测试人员便应声到,“这样交互好像不是很友好吧!”,而此时我关心的并不是交互的问题,而是这个崩溃何解,便随意答应了一下,说“那我去看一下代码。”
打开源码,简单几句代码出现在了眼前:
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 再创建,周而复始,却一直没有退出过一个消息循环,当然也就没有返回过函数,我们知道,函数只调用不返回,当然堆栈会一直向下涨,最后造成堆栈溢出。
回忆一下,测试描述的问题是,隔一定时间,程序必崩,这也符合了崩溃场景,因为拷机是以固定时间来产生这个消息,而调用环境相同,那么栈增长速度必然相同,栈大小固定,那么隔固定时间后,也必然会引起栈溢出,至此,这个崩溃被定位,那解决方法就很简单了,我们采用了一位同事的非常好的建议,像这种不需要用户确定的通知型消息,根本不用模态框来完成,所以直接使用了 Pop 框通知一下,最后也由那位同事(weilaitao)行了代码的修改。
堆栈溢出的问题有时是非常头疼的,而像这种由特殊业务造成的间接性的栈溢出也是少见,且查且珍惜。