【Bug现象】:
使用搜狗浏览器release1.0.1时,如果新建多个标签页浏览页面时,偶尔会出现工具栏的前进、后退、刷新等按钮为彩色可点击状态,但是点击之后页面却没有发生相应的动作。
【Bug背景】:
搜狗浏览器提供多标签方式浏览页面,每个标签即对应一个工作线程。每个工作线程都维护一个自己的IWebBrowser2接口。当用户点击工具栏的前进、后退、刷新等按钮时,工具栏发送相应的消息给UI线程,UI线程再将消息分发给对应的工作线程,工作线程在响应消息时会调用IE接口IWebBrowser2相应的函数,如GoForward,Goback,ReFresh等,最终在容器CAxControl (即对应页面区)显示结果。如果用户点击页面中的某个链接为target=”_blank”时(即在新窗口打开),CWebBrowserEventsManager会探测到IE的回调事件DISPID_NEWWINDOW2。此时浏览器会创建一个新的标签页同时也会创建一个新的工作线程,在此过程中UI线程会共享使用新线程的IWebBrowser2接口。
【知识补充】:
多线程使用COM接口时,不要在线程之间传递原始接口指针
如果一个线程要与另一个线程共享一个接口指针,它应首先封送该接口指针。如果有必要,封送接口指针可使 COM 创建一个新的代理(以及一个新的信道对象,将代理和存根结对),以允许从另一个单元向外调用。不通过封送而将原始接口指针(内存中的一个 32 位地址)传递给另一个线程,会绕过 COM 的并发机制,并且如果发送和接收的线程位于不同的单元中,将出现各种不良行为。(在 Windows 2000 中,由于两个对象可以共享一个单元,但又位于不同的上下文中,因此如果线程位于同一个单元中,可能会使您陷入困境。)典型的症状包括调用失败和返回 RPC_E_WRONG_THREAD_ERROR。
Windows NT 4.0 和更高版本可以使用一对名为 **CoMarshalInterThreadInterfaceInStream** 和 **CoGetInterfaceAndReleaseStream** 的 API 函数,在线程之间轻松地封送接口指针。假定您应用程序中的一个线程(线程 A)创建了一个 COM 对象,继而接收了一个 IFoo 接口指针,并且同一进程中的另一个线程(线程 B)想调用这个对象。在准备将接口指针传递给线程 B 时,线程 A 应该封送该接口指针,如下所示:
CoMarshalInterThreadInterfaceInStream (IID_IFoo, pFoo, &pStream); //列集
在 CoMarshalInterThreadInterfaceInStream 返回后,线程 B 就可以安全地取消封送该接口指针:
IFoo* pFoo;
CoGetInterfaceAndReleaseStream (pStream, IID_IFoo, (void**) &pFoo); //散集
在这些示例中,pFoo 是一个 IFoo 接口指针,pStream 是一个 IStream 接口指针。COM 在调用 CoMarshalInterThreadInterfaceInStream 时初始化 IStream 接口指针,然后在 CoGetInterfaceAndReleaseStream 内部使用和释放该接口指针。实际上,您通常要使用一个事件或其他同步化基元来协调这两个线程的行为。例如,让线程 B 知道接口指针已准备好,可以取消封送。
请注意,以这种方式封送接口指针不会出现任何问题,因为 COM 有足够的智能,在不需要进行封送时不会去封送(或重新封送)指针。如果在线程之间传递接口指针时这样做,使用 COM 就轻松多了。
【Bug原因】:
release1.0.1版本UI线程在共享使用工作线程的IWebBrowser2接口时,没有经过列集和散集的方式传递时,而是直接传递原始指针,造成上述BUG现象。
例如浏览器在处理IE回调事件DISPID_NEWWINDOW2时会执行以下代码:
HRESULT CWebBrowserEventsManager::OnNewWindow2(REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
if (pDispParams->cArgs < 2)
{
return E_FAIL ;
}
IWebBrowser2* pWebBrowser2 ;
CComPtr spMainUI ;
CComPtr spTraveler ;
HRESULT hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_Main_UI, __uuidof(IMainUI), (void**)&spMainUI);
hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_CTraveler, __uuidof(ITraveler), (void**)&spTraveler);
if (!spMainUI || !spTraveler)
{
return S_FALSE ;
}
DWORD dwID = 0 ;
CString cstrUrl = L"" ;
if (m_pCustomSite)
{
CComBSTR bstrUrl ;
m_pCustomSite->GetNavigateUrl(bstrUrl) ;
cstrUrl = bstrUrl ;
}
spMainUI->CreateNewTab(cstrUrl, (DWORD)&dwID) ;
spTraveler->GetWebBrowserByID(dwID, (IUnknown**)&pWebBrowser2) ; //此处直接使用了原始指针
if (!pWebBrowser2)
{
return S_FALSE ;
}
IDispatch** ppDisp = pDispParams->rgvarg[1].ppdispVal;
hr = pWebBrowser2->get_Application(ppDisp);
return S_OK ;
}
除此之外,所有会产生新标签页的用户的操作都会涉及到多线程使用IE接口的问题,如点击主页按钮等。
【解决办法】:
加入列集和散集操作,并加入多线程同步处理函数。
1. 在主线程加入多线程同步处理代码:
…
for (;;)
{
if (::MsgWaitForMultipleObjectsEx(1, &hEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE) != WAIT_OBJECT_0)
{
MSG msg;
BOOL bRet = ::PeekMessage(&msg, m_hWnd, 0, 0, PM_REMOVE);
if (bRet)
{
if (msg.message == WM_QUIT)
break;
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
}
…
- 加入列集处理函数:
IStream* CAxControl::GetMarshalAxControl()
{
try
{
//特别注意不要在单线程的情况下掉这个函数
IStream* pStream = NULL ;
CComPtr spWebBrowser2 ;
HRESULT hr = QueryAxControl(IID_IWebBrowser2, (void**)&spWebBrowser2) ;
if (FAILED(hr) || !spWebBrowser2)
{
return NULL ;
}
hr = CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, spWebBrowser2, &pStream) ;
if (FAILED(hr) || !pStream)
{
return NULL ;
}
return pStream ;
}
catch (...)
{
ATLASSERT(FALSE) ;
return NULL ;
}
}
- 在涉及到跨线程调用IE接口的地方加入GetMarshalAxControl()函数,例如浏览器在处理IE回调事件DISPID_NEWWINDOW2时:
HRESULT CWebBrowserEventsManager::OnNewWindow2(REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
if (pDispParams->cArgs < 2)
{
return E_FAIL ;
}
IWebBrowser2* pWebBrowser2 = NULL ;
CComPtr spMainUI ;
CComPtr spTraveler ;
HRESULT hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_Main_UI, __uuidof(IMainUI), (void**)&spMainUI);
hr = Util::ModuleManager::CreateInstance(CLSID_Sogou_CTraveler, __uuidof(ITraveler), (void**)&spTraveler);
if (!spMainUI || !spTraveler)
{
return S_FALSE ;
}
DWORD dwID = 0 ;
CString cstrUrl = L"" ;
if (m_pCustomSite)
{
CComBSTR bstrUrl ;
m_pCustomSite->GetNavigateUrl(bstrUrl) ;
cstrUrl = bstrUrl ;
}
if (m_bForceBackOpen)
{
spMainUI->CreateNewTab(cstrUrl, (DWORD)&dwID, FALSE, m_dwChildFrameID) ;
m_bForceBackOpen = FALSE ;
}
else
{
spMainUI->CreateNewTab(cstrUrl,(DWORD)&dwID,GetAsyncKeyState(VK_CONTROL) < 0 ? FALSE : TRUE, m_dwChildFrameID) ;
}
IStream* pStream = NULL ;
CAxControl* p = (CAxControl*)::GetWindowLongPtr(::FindWindowEx((HWND)dwID, NULL, NULL, NULL), GWLP_USERDATA) ;
::SendMessageTimeout(p->m_hWnd, WM_USER_GET_WEBBROWSER2_CROSS_THREAD, 0, 0, SMTO_NORMAL, 2000, (PDWORD_PTR)&pStream) ;//发送消息给当前标签页,使其调用列集函数封送m_spWebBrowser2指针
CoGetInterfaceAndReleaseStream(pStream, IID_IWebBrowser2, (void**)&pWebBrowser2) ;//散集操作得到m_spWebBrowser2指针
if (!pWebBrowser2)
{
return S_FALSE ;
}
pWebBrowser2->AddRef() ;
IDispatch** ppDisp = pDispParams->rgvarg[1].ppdispVal;
hr = pWebBrowser2->get_Application(ppDisp);
return S_OK ;
}
【Bug总结】:
- 在设计浏览功能用例时,考虑到了对当前标签页的操作可能会影响到其他标签页,用例虽然覆盖到了,但是实际发现这个BUG是在随机测试中,因为此BUG的触发条件是不定的。因此,对于可能涉及到多线程的用例,尽量多执行几次用例或者使用自动化测试工具进行测试。