测试代码/问题表现
local node = cc.ParticleSystemQuad:create(filename)
local size = cc.Director:getInstance():getWinSize()
node:setPosition(cc.p(size.width / 2, size.height / 2))
scene:addChild(node)
每创建一个粒子,就会发生一次泄露,使用leak-tracer检测内存泄露,指向的位置为Image.cpp,起初我以为是误报,后来经过验证,也就是将image的malloc地址缓存起来,image析构的时候再移除,最后观察是否还有image没有移除,确认就是image造成的内存泄露。
问题定位
只有在Android平台,才启用了CC_ENABLE_CACHE_TEXTURE_DATA,看到网上有说这个是为了安卓切换到后台再返回的时候,避免因为重新加载纹理导致的黑屏问题。
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
#define CC_ENABLE_CACHE_TEXTURE_DATA 1
#else
#define CC_ENABLE_CACHE_TEXTURE_DATA 0
#endif
在CCParticleSystem中,解析plist并加载设置纹理
if( dictionary.find("textureImageData") != dictionary.end() ){
// ...
// For android, we should retain it in VolatileTexture::addImage which invoked in Director::getInstance()->getTextureCache()->addUIImage()
image = new (std::nothrow) Image(); //image.ref=1 就是这个new导致的泄露
bool isOK = image->initWithImageData(deflated, deflatedLen);
CCASSERT(isOK, "CCParticleSystem: error init image with Data");
CC_BREAK_IF(!isOK);
// texture.ref=1,CC_ENABLE_CACHE_TEXTURE_DATA的原因,image.ref=2
auto texture = Director::getInstance()->getTextureCache()->addImage(image, _plistFile + textureName);
setTexture(texture);// texture.ref=2
image->release();// image.ref=2 无法释放
}
注释中也说明了,在Android平台,Image会额外的retain一次,所以这个的image->release()是无法释放的,乍一看应该是这个image的问题,其实不然,解析往后看。
image->release()无法释放的原因,CCTextureCache.cpp中相关代码:
Texture2D* TextureCache::addImage(Image* image, const std::string& key){
// ...
#if CC_ENABLE_CACHE_TEXTURE_DATA
VolatileTextureMgr::addImage(texture, image); // 重点逻辑
#endif
}
std::list<VolatileTexture*> VolatileTextureMgr::_textures;
void VolatileTextureMgr::addImage(Texture2D* tt, Image* image)
{
if (tt == nullptr || image == nullptr)
return;
// 相当于Texture2D和VolatileTexture是一一映射的关系
VolatileTexture* vt = findVolotileTexture(tt);
image->retain(); // retain
// 同一个plist,第二次创建时texture都是同一个,因为都是从TextureCache中获取的,所以上边的vt也是同一个
// 但是image发生了变化,直接赋值就让之前的image变成了野指针
// CC_SAFE_RELEASE_NULL(vt->_uiImage); // 修复内存泄露的代码
vt->_uiImage = image;
vt->_cashedImageType = VolatileTexture::kImage;
}
VolatileTexture* VolatileTextureMgr::findVolotileTexture(Texture2D* tt)
{
VolatileTexture* vt = nullptr;
for (const auto& texture : _textures)
{
VolatileTexture* v = texture;
if (v->_texture == tt)
{
vt = v;
break;
}
}
if (!vt)
{
vt = new (std::nothrow) VolatileTexture(tt);
_textures.push_back(vt);
}
return vt;
}
VolatileTexture::VolatileTexture(Texture2D* t)
: _texture(t) // 对应的纹理
{
}
粒子销毁时,析构函数的逻辑,因为粒子对应的texture在TextureCache中有一份,所以粒子的texture在游戏过程中永远也无法释放,除非手动清理TextureCache,
ParticleSystem::~ParticleSystem()
{
_particleData.release();
CC_SAFE_RELEASE(_texture);// texture.ref=2,texture无法释放
}
Texture2D::~Texture2D()
{
#if CC_ENABLE_CACHE_TEXTURE_DATA
VolatileTextureMgr::removeTexture(this);// 导致vt绑定的_uiImage也无法释放
#endif
}
void VolatileTextureMgr::removeTexture(Texture2D* t)
{
for (auto& item : _textures)
{
VolatileTexture* vt = item;
if (vt->_texture == t)// 对应的纹理
{
_textures.remove(vt);
delete vt;
break;
}
}
}
VolatileTexture::~VolatileTexture()
{
CC_SAFE_RELEASE(_uiImage);
}
整理下思路: TextureCache::addImage的逻辑中存在一条映射链
image / particle.texture / VolatileTexture._texture / VolatileTexture._uiImage
按照这个逻辑推理,如果image想要释放那么只需要particle.texture释放即可,很明显particle.texture释放不了,原因我上边的注释也写了,因为TextureCache的原因,注释我已经写的很清楚了,可以回过头再仔细理解下。
所以,问题就变成了我们不能指望着particle.texture释放,它也释放不了,所以保存image指针前,就需要看texture绑定的是否有_uiImage,有的话,就把之前的释放掉,这样简单粗暴的就修复了内存泄露的问题。
扩展
重复创建同一个粒子,你会发现其实这个image的内容没有发生变化,还有一种修复办法就是CC_ENABLE_CACHE_TEXTURE_DATA模式下把image缓存起来,粒子纹理从image cache中取,还能提升一定的性能,可扩展的修复结果:
void VolatileTextureMgr::addImage(Texture2D* tt, Image* image)
{
if (tt == nullptr || image == nullptr)
return;
VolatileTexture* vt = findVolotileTexture(tt);
image->retain();
// release previous image, otherwise case memory leak
if (vt->_uiImage != image){ //万一image是从cache里面取的呢?
CC_SAFE_RELEASE_NULL(vt->_uiImage);
}
vt->_uiImage = image;
vt->_cashedImageType = VolatileTexture::kImage;
}
Sprite为啥没有这个问题呢?
Texture2D* addImage(Image *image, const std::string &key);// 有vt
Texture2D* addImage(const std::string &filepath, bool bSpriteFrame);// sprite调用的是这个,没有vt