COM对象除了引用计数还有...

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对象的生命周期内, 也一定要记得调用, 否则就会像我一样花一整天的时间定位崩溃.

 

posted on 2015-03-10 15:27  静夜繁星  阅读(258)  评论(0编辑  收藏  举报

导航