COM对象除了引用计数还有…
一. 背景:
VideoManager支持实时, 需要同时传入一组窗口的设备信息和StreamerID, 并且传入之后需要设置给相对应的VideoView. 所以在VideoManager实现了IDeviceInfo的COM对象, 包含三个成员分别是IVideoView* video_view, GUID streamer_id, IDeviceConfig* device_config.
C#的调用如下
var list_deviceInfo = new List<LiveDeviceInfo>(); foreach (var videoViewPort in videoViewPorts) { videoViewPort.PlayMode = VideoPlayMode.Live; var videoViewWrapper = videoService.Get(videoViewPort); if (videoViewWrapper != null) { videoViewWrapper.PlayMode = VideoPlayMode.Live; } var liveDevice = new LiveDeviceInfo(); liveDevice.streamer_id = videoViewPort.PlayingDevice.Id; liveDevice.video_view = (VideoView)videoViewWrapper.VideoView; liveDevice.device_info = videoViewPort.PlayingDevice; list_deviceInfo.Add(liveDevice); } SyncVideoService.SetLiveConfig(Layout, list_deviceInfo.ToArray()); |
二. 现象:
调用的前两次都正常, 到第三次出现了崩溃. 而且每次都是如此. 崩溃的异常和堆栈在CLR中, 由于VS2010同时查看Manage和Native看到的堆栈信息不全, 所以使用Windbg的命令!dumpstack查看, 发现崩溃在clr中, 由于没有源代码很难捕捉到更多信息.
ChildEBP RetAddr Caller, Callee 0019df28 6144d5ed clr!SafeAddRef+0x53 0019df3c 6144d625 clr!RCW::GetComIPForMethodTableFromCache+0x25f, calling clr!SafeAddRef 0019df4c 612e826c clr!ObjHeader::GetSyncBlock+0x33, calling clr!ObjHeader::PassiveGetSyncBlock 0019df70 612f1c0c clr!JIT_GetSharedGCThreadStaticBase+0x28, calling clr!GetThread 0019df88 6144d431 clr!RCW::GetComIPFromRCW+0x2d, calling clr!RCW::GetComIPForMethodTableFromCache 0019df98 61388bc9 clr!GetComIPFromObjectRef+0x1e4, calling clr!RCW::GetComIPFromRCW 0019dff8 6145510b clr!MarshalObjectToInterface+0x3a, calling clr!GetComIPFromObjectRef 0019e008 6145509f clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0xd8, calling clr!MarshalObjectToInterface 0019e030 613738ad clr!StubHelpers::DemandPermission+0x133, calling clr!LazyMachStateCaptureState 0019e070 61455049 clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x73, calling clr!LazyMachStateCaptureState |
三. 诊断
为了缩小问题的范围, 先尝试将C#中的调用多次重复操作, 发现问题发生在SyncVideoService.SetLiveConfig; 于是查看这一块的C++代码.
for (int i=low_bound; i<= high_bound; i++) { LPUNKNOWN ptr_unknown = safearray_deviceinfos.GetAt(i); if (FAILED(ptr_unknown->QueryInterface(IID_ILiveDeviceInfo, (void**)(&live_device_info)))) { break; } VARIANT_BOOL rst; IVideoView* video_view = NULL; live_device_info->get_video_view(&video_view); live_device_info->get_device_info(&device_config); live_device_info->get_streamer_id(&streamer_id); video_view->SetLiveVideoConfig(streamer_id, device_config, &rst); } |
继续增加C+这一块的重复调用, 终于发现get_video_view的重复导致的问题. 只需要对它反复调用三次就会崩溃.
IVideoView* video_view = NULL; live_device_info->get_video_view(&video_view); live_device_info->get_video_view(&video_view); live_device_info->get_video_view(&video_view); |
于是观察get_video_view, 它的内部实现很简单, 只是通过IVideoView**返回一个指针值. 在内部下断点跟踪第三次, 内部复制依然正常, 可返回到调用方就报异常, 并且对象为NULL.
STDMETHODIMP CLiveDeviceInfo::get_video_view(IVideoView** pVal) { *pVal= i_video_view_; return S_OK; } |
在c:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\atlmfc\include\atlcom.h
ULONG InternalAddRef();
ULONG InternalRelease();
代码下断点查看这个对象的引用值, 发现引用值没有发生额外的变化. 并且m_dwRef数值在8~10之间变化, 并没有被减到0. 在该COM对象的FinalRelease()下断点, 也没有断下来.
查看了一些关于COM对象引用计数的文章, 了解到对于从某接口返回COM对象之前, 需要调用AddRef(), 或QueryInterface()来为返回的对象增加引用计数. 在使用完该对象之后, 使用Release()释放引用.
修改如下:
STDMETHODIMP CLiveDeviceInfo::get_video_view(IVideoView** pVal) { i_video_view_->QueryInterface(IID_IVideoView, (void**)pVal); return S_OK; } |
live_device_info->get_video_view(&video_view); … video_view->Release(); |
经过测验, 问题得到解决.
四. 疑问:
进入atlcom.h内部查看引用计数没有任何问题. 可关于引用计数的接口调用却可以解决这个问题. 所以, 推测除了引用计数, 应该还有其它东西在管理这COM对象.
于是, 在崩溃前一刻下断点开启全部exception, 抓取到一个异常, 堆栈如下, 查看到CStdMarshal::MarshalObjRef, 这是报异常的第一刻, 这一刻意味着返回COM对象的中间过程可能要经过一些对COM对象管理对象的修改. 对CStdMarshal::MarshalObjRef这个windows内部函数进行查找, 可没有找到相关的有效的相关信息.(如果谁知道这里面包含什么, 请告知我)
Current frame: KERNELBASE!RaiseException+0x58 ChildEBP RetAddr Caller, Callee 004ecc10 761fc41f KERNELBASE!RaiseException+0x58, calling ntdll!RtlRaiseException 004ecc34 77cfe023 ntdll!RtlFreeHeap+0x105, calling ntdll!RtlpLowFragHeapFree 004ecc4c 76f6f18c ole32!operator delete+0x16, calling ntdll!RtlFreeHeap 004ecc58 75b35c93 rpcrt4!RpcpRaiseException+0x7b, calling kernel32!RaiseExceptionStub 004ecc74 76f94387 ole32!CStdMarshal::MarshalObjRef+0x11e, calling rpcrt4!RpcRaiseException 004eccb8 75b31cf1 rpcrt4!NdrpPointerMarshall+0x90 004eccdc 75b26b24 rpcrt4!NdrPointerMarshall+0x30, calling rpcrt4!NdrpPointerMarshall 004ecd18 75b26b24 rpcrt4!NdrPointerMarshall+0x30, calling rpcrt4!NdrpPointerMarshall 004ecd5c 75bc06b8 rpcrt4!NdrStubCall2+0x402, calling rpcrt4!NdrpServerMarshal 004ecd8c 77d340b3 ntdll!RtlpFindGuidInSection+0xac, calling ntdll!__security_check_cookie 004ecdb8 76f88e72 ole32!NdrpOleAllocate |
五. 结论:
将COM对象返回给外部使用时, 一定要使用AddRef(), 或QueryInterface()接口, 确保传递前对COM对象相关底层管理对象都有设置好.返回的对象在使用完之后, 调用Release(), 防止泄露.
即使明知道这一次返回的指针肯定在该COM对象的生命周期内, 也一定要记得调用, 否则就会像我一样花一整天的时间定位崩溃.