一、问题提出
Mapx图层的重绘是通过响应MapX的用户自定义图层的绘制消息进行的,在消息响应函数中循环遍历地图上加载的图层,调用符合条件的图层的Draw函数进行绘制。使用此机制过程中,发现图层刷新时,内存增长很快,平均每秒钟增长8K左右。最终将问题定位到如下语句:
WideString ws = m_map->Layres->Item(i+1)->Name;
由于问题语句中“=”号右边返回的是一个BSTR类型值,所以先对BSTR类型做一个简单的介绍。
BSTR类型是为了跨语言平台使用而存在的一种宽字符集字符串指针,标准BSTR是一个有长度前缀和null结束符的OLECHAR数组,其定义如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
0a 00 00 00 H E L L O \0
BSTR包含四个字节的长度前缀,BSTR的前4字节是一个表示字符串长度的前缀。BSTR长度域的值是字符串的字节数,并且不包括0结束符。由于BSTR类型包含了长度前缀,所以需要通过SysAllocString函数进行内存分配,通过SysFreeString函数进行内存释放。其中SysAllocString函数返回的指针指向BSTR第一个字符,而不是BSTR的内存首地址。
由于BSTR被用于COM接口,申请和使用者通常不是同一个,所以在使用过程中要求使用者将其释放。
m_map为MapX的一个COM对象,m_map->Layers->Item(i + 1)->Name返回的是一个BSTR类型值。该值在返回前由m_map对象通过调用系统函数SysAllocString生成,并要求接收该类型值的对象将其释放。这就要求使用者不仅仅完成值的使用,而且需要完成内存释放的任务。
在程序中,调用了WideString的拷贝构造函数,那么WideString对象只是使用了内存的内容,而并没有对这块内存进行释放。程序中也没有显示的内存释放操作,从而导致了内存的泄露。
由于BSTR所存在的申请者和释放者不同的特点,那么WideString向BSTR就必然存在内存二次释放的潜在问题。当调用一个需要BSTR类型入参的COM对象方法时,代码中原来的实现是:
m_map->Expoert(WideString(L"clipboard"), miFormatBMP);
这条语句在执行时,先由WideString类申请了内存,返回一个WideString对象,然后调用COM对象的ExportMap函数,ExportMap函数使用完内存后对将内存进行释放,而在函数调用完毕后,WideString对象又会对内存进行一次释放。这样就对一块内存有两次释放操作,可能一般情况下并不会发生异常,但还是存在潜在问题,给程序埋下了隐患。
二、解决与预防措施
可以采用两种方案解决以上问题,一种是手动申请或释放内存,另一种是让WideString来完成内存的申请或释放工作。
方案一:手动进行内存申请或释放
在使用BSTR类型前,先定义一个BSTR指针bstr作为中间变量,进行数据传递。而在使用完该指针后,调用::SysFreeString函数将bstr指针指向的内存空间释放掉。代码如下:
BSTR bstr = m_map->Layers->Item(i+1)-Name;
WideString ws(bstr);
::SysFreeString(bstr);
在调用需要输入BSTR类型参数的COM对象方法时,也可以采用相同的办法。即定义一个BSTR类型中间变量bstr,给它申请一块内存,然后传递给COM对象,最后让COM对象将内存释放。代码如下:
BSTR bstr = ::SysAllocString(L"clipboard");
m_map->ExportMap(bstr, miFormatBMP);
由于这种方案只能看到内存的申请或者释放函数,所以代码走查中可能会错误的认为代码有错。
该方案的优点在于,内存的释放由代码自身决定,不受ws生命周期的限制。缺点是,如果用户忘记了谢申请和释放函数还是一样会产生内存泄漏问题。
方案二:让WideString完成内存的申请或释放
WideString的拷贝构造函数不能达到内存指针直接引用的目的,但它提供了一个Attach函数。该函数就是将WideString的内部指针直接指向了需要引用的内存地址,然后析构的时候将这块内存释放掉了。该函数定义如下:
void _fastcall WideString::Attach(BSTR src)
{
_ASSERT(Data == 0);
Data = src;
}
采用Attach函数将代码修改如下:
WideString ws;
ws.Attach(m_map->Layers->Item(i+1)->Name);
同时,在WideString向BSTR类型赋值时,WideString也提供了一个有用的Detach函数,该函数在内部定义一个BSTR类型指针,指向内部成员Data,然后将Data赋值为空,最后返回该BSTR指针。函数定义如下:
BSTR _fastcall WideString::Detach()
{
BSTR bstr = Data;
Data = 0;
return bstr;
}
在调用需要输入BSTR类型参数的COM对象方法时,可以这样使用:
WideString ws("clipboard");
m_map->ExportMap(ws.Detach(), miFormatBMP);
该方案优点在于,直接将内存释放工作交给WideString,不会发生潜在的内存泄漏问题。而缺点是不是特别灵活。
注意,由于Attach直接将内部成员Data指向了新的内存单元,如果原来ws的内容不为空,就会发生内存泄漏问题。所以Attach语句执行前一定要保证ws的内容为空。