Cocos2d-x 3.x基础学习:内存管理机制
在3.x版本中,Cocos2d-x采用全新的根类Ref来实现类对象的引用计数记录,引擎中的所有类均派生自Ref。
1、引用计数
引用计数的概念可参考《维基百科》:引用计数。Cocos2d-x通过引用计数来管理内存,具体操作如下:
- 调用
retain()方法:使对象的引用计数加1,表示获取该对象的引用权。 - 调用
release()方法:在引用结束时,使对象的引用计数值减1,表示释放该对象的引用权。 - 调用
autorelease()方法:将对象放入自动释放池。当释放池自身被释放时,会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。Cocos2d-x提供了AutoreleasePool来管理自动释放对象。
核心类Ref实现了引用计数,以下是其代码定义:
// CCRef.h
class CC_DLL Ref
{
public:
void retain(); // 保留。引用计数+1
void release(); // 释放。引用计数-1
Ref* autorelease(); // 实现自动释放。
unsigned int getReferenceCount() const; // 被引用次数
protected:
Ref(); // 初始化
public:
virtual ~Ref(); // 析构
protected:
unsigned int _referenceCount; // 引用次数
friend class AutoreleasePool; // 自动释放池
};
// CCRef.cpp
// 节点被创建时,引用次数为 1
Ref::Ref() : _referenceCount(1)
{
}
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
++_referenceCount;
}
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
delete this;
}
}
Ref* Ref::autorelease()
{
// 将节点加入自动释放池
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
Ref原理分析
- 当一个
Ref对象被初始化(即被new出来时),_referenceCount初始值为1。 - 当调用该
Ref对象的retain()方法时,_referenceCount加1。 - 当调用该
Ref对象的release()方法时,_referenceCount减1。 - 若
_referenceCount减为0,则删除该Ref对象。
2、retain()和release()的使用
下面通过一个简单的例子来学习retain()和release()的使用:
TestObject* obj1 = new TestObject("testobj1");
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->retain();
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->release();
CCLOG("obj1 referenceCount=%d", obj1->getReferenceCount());
obj1->release();
控制台显示的日志如下:
cocos2d: TestObject:testobj1 is created
cocos2d: obj1 referenceCount=1
cocos2d: obj1 referenceCount=2
cocos2d: obj1 referenceCount=1
cocos2d: TestObject:testobj1 is destroyed
通过上述例子和打印结果可以看出:
obj1对象创建后,引用计数为1。- 执行一次
retain()后,引用计数变为2。 - 执行一次
release()后,引用计数回到1。 - 再执行一次
release()后,对象被释放。
因此,我们可以通过调用retain()方法获取对象的引用权,在引用结束时调用release()方法释放引用权,直到对象的引用计数为0时,对象被释放。
3、autorelease()的使用
同样通过一个简单的例子来学习autorelease()的使用,代码如下:
TestObject* obj = new TestObject("testobj");
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->autorelease();
CCLOG("obj is add in currentpool %s", PoolManager::getInstance()->getCurrentPool()->contains(obj) ? "true" : "false");
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->retain();
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
obj->release();
CCLOG("obj referenceCount=%d", obj->getReferenceCount());
// obj in current pool will be release
Director::getInstance()->replaceScene(this);
控制台显示的日志如下:
cocos2d: TestObject:testobj is created
cocos2d: obj referenceCount=1
cocos2d: obj is add in currentpool true
cocos2d: obj referenceCount=1
cocos2d: obj referenceCount=2
cocos2d: obj referenceCount=1
...
cocos2d: TestObject:testobj is destroyed
通过代码和打印结果可知:
obj对象创建后,引用计数为1。- 执行一次
autorelease()后,obj对象被加入到当前的自动释放池,但引用计数值并未减1。 - 在下一帧开始前,当前的自动释放池会被回收,并对池中的所有对象执行一次
release()操作。 - 当对象的引用计数为0时,对象会被释放。
obj对象执行autorelease()后,又执行了一组retain()和release()操作,此时引用计数仍为1。在场景切换后,当前的自动释放池被回收,obj对象执行一次release()操作后引用计数减为0,对象被释放。
需要注意的是,autorelease()只有在自动释放池被释放时才会进行一次释放操作。如果对象释放的次数超过了应有的次数,这个错误在调用autorelease()时不会被发现,只有当自动释放池被释放时(通常是游戏的每一帧结束时),游戏才会崩溃,此时定位错误会非常困难。例如,在游戏中一个对象含有1个引用计数,但被调用了两次autorelease(),第二次调用时游戏会继续执行这一帧,结束游戏时才会崩溃,很难及时找到出错的地点。因此,在开发过程中应避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量使用release()来释放对象引用。
4、AutoreleasePool类的使用
Cocos2d-x提供了AutoreleasePool来管理自动释放对象,下面通过一个简单的例子讲解其使用方法:
TestObject* obj2 = new TestObject("testobj2");
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
// use AutoreleasePool
{
AutoreleasePool pool;
obj2->retain();
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
obj2->release();
CCLOG("obj2 referenceCount=%d", obj2->getReferenceCount());
obj2->autorelease();
CCLOG("obj2 is add in pool %s", pool.contains(obj2) ? "true" : "false");
TestObject *obj3 = new TestObject("testobj3");
obj3->autorelease();
CCLOG("obj3 is add in pool %s", pool.contains(obj3) ? "true" : "false");
}
控制台输出日志如下:
cocos2d: TestObject:testobj2 is created
cocos2d: obj2 referenceCount=1
cocos2d: obj2 referenceCount=2
cocos2d: obj2 referenceCount=1
cocos2d: obj2 is add in pool true
cocos2d: TestObject:testobj3 is created
cocos2d: obj3 is add in pool true
cocos2d: TestObject:testobj2 is destroyed
cocos2d: TestObject:testobj3 is destroyed
通过代码和输出结果可以看到:
- 创建
obj2对象时,其引用计数为1。 - 接着创建一个自动释放池,对
obj2对象执行retain()和release()操作后,再执行autorelease()操作,此时obj2对象被加入到当前新建的自动释放池。 - 新建
obj3对象并执行autorelease()操作,obj3也被加入到当前新建的自动释放池。 - 在代码块结束后,自动释放池被回收,
obj2和obj3执行release()操作,引用计数减为0,对象被释放销毁。
我们可以自己创建AutoreleasePool来管理对象的autorelease操作。虽然Cocos2d-x保证每一帧结束后释放一次释放池,并在下一帧开始前创建一个新的释放池,但如果在一帧之内生成了大量的autorelease对象,会导致释放池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,最好手动创建并释放一个回收池,示例如下:
// example of using temple autorelease pool
{
AutoreleasePool pool2;
char name[20];
for (int i = 0; i < 100; ++i)
{
snprintf(name, 20, "object%d", i);
TestObject *tmpObj = new TestObject(name);
tmpObj->autorelease();
}
}
总结
autorelease()的实质是将对象加入自动释放池,对象的引用计数不会立刻减1,而是在自动释放池被回收时执行release()操作。autorelease()并非毫无代价,其背后的释放池机制会占用内存和CPU资源。- 过多使用
autorelease()会增加自动释放池的管理和维护成本,在内存和CPU资源本就不足的程序中会使系统资源更加紧张。 - 此时需要合理创建自动释放池来管理对象的
autorelease操作,对于不用的对象,推荐使用release()来释放引用,立即回收。
5、特殊内存管理
5.1、工厂方法create()
在Cocos2d-x中,提供了大量的工厂方法来创建对象,这些对象通常都是自动释放的。以Label的create方法为例:
Label* Label::create()
{
auto ret = new Label();
if (ret)
{
ret->autorelease();
}
return ret;
}
可以发现,创建Label对象后,对其执行了autorelease()操作,表示该对象是自动释放的。Layer、Scene、Sprite等类的create()方法也类似。使用工厂方法创建对象时,虽然引用计数为1,但由于对象已被放入释放池,调用者没有对该对象的引用权,除非人为调用retain()来获取引用权,否则不需要主动释放对象。
5.2、Node的addChild() / removeChild方法
在Cocos2d-x中,所有继承自Node类的对象,在调用addChild方法添加子节点时,会自动调用retain;通过removeChild移除子节点时,会自动调用release。调用addChild方法添加子节点时,节点对象执行retain,子节点被加入到节点容器中,父节点销毁时会销毁节点容器并释放子节点,对子节点执行release。如果想提前移除子节点,可以调用removeChild。在大部分情况下,通过调用addChild/removeChild方法可以自动完成retain和release调用,无需再手动调用。
【内存优化】
1、内存优化原理
为优化应用内存使用,开发人员首先要了解什么最耗应用内存,答案是纹理,纹理几乎会占据90%的应用内存。因此,应尽量最小化应用的纹理内存使用,否则应用可能会因低内存而崩溃。本节介绍Cocos2d-x游戏通用的两条内存优化原理。
1.1、认识瓶颈寻找方案
要了解什么样的纹理最耗应用内存以及这些纹理会消耗多少内存,可使用苹果的工具“Allocation & Leaks”。在Xcode中长按“Run”命令,选择“Profile”即可启动这两个工具。使用Allocation工具可以监控应用的内存使用,使用Leaks工具可以观察内存的泄漏情况。此外,还可以通过一些代码获取游戏内存使用的其他信息,示例如下:
Sprite* bg = Sprite::create("HelloWorld.png");
bg->setPosition(240, 160);
this->addChild(bg);
CCLOG("%s", Director::getInstance()->getTextureCache()->getCachedTextureInfo().c_str());
调用上述代码后,游戏在DEBUG模式下运行,在Xcode控制台窗口会看到格式工整的日志信息,例如:
cocos2d: "****/HelloWorld.png" rc=2 id=3 480 x 320 @ 32 bpp => 600 KB
"/cc_fps_images" rc=5 id=2 999 x 54 @ 16 bpp => 105 KB
TextureCache dumpDebugInfo: 2 textures, for 705 KB (0.69 MB)
从日志中可以看到纹理的名称、引用计数、ID、大小及每像素的位数,最重要的是能显示内存的使用情况,如“cc_fps_images”消耗了105KB内存,“HelloWorld.png”消耗了600KB内存。
1.2、切勿过度优化
这是一个通用的优化规则。在优化过程中,需要进行权衡取舍,因为图像质量和图像内存使用往往是相互矛盾的,千万不要过度优化。
2、内存优化等级
将Cocos2d-x内存优化分为三个等级,每个等级的说明和策略不同。
2.1、客户端等级
这是最重要的优化等级,因为在Cocos2d-x引擎顶层编译游戏时,引擎自身会提供一些优化选项,在这个等级可以进行大部分优化,主要包括优化纹理、音频、字体及粒子的内存使用。
- 纹理优化:影响纹理内存使用的主要因素有纹理格式(压缩还是非压缩)、颜色深度和大小。可以使用PVR格式纹理减少内存使用,推荐纹理格式为
pvr.ccz。纹理使用的每种颜色位数越多,图像质量越好,但越耗内存,因此可以使用颜色深度为RGB4444的纹理代替RGB8888,可使内存消耗降低一半。此外,超大的纹理会导致内存相关问题,最好使用中等大小的纹理。 - 音频优化:影响音频文件内存使用的因素有音频文件数据格式、比特率及采样率。推荐使用MP3数据格式的音频文件,因为Android平台和iOS平台均支持MP3格式,且MP3格式经过压缩和硬件加速。背景音乐文件大小应低于800KB,可通过减少背景音乐时间然后重复播放来实现。音频文件采样率大约在96 - 128kbps为佳,比特率44kHz即可。
- 字体和粒子优化:使用BMFont字体显示游戏分数时,应尽可能使用最少数量的文字,例如只显示单位数的数字时,可以移除所有字母。对于粒子,可以通过减少粒子数来降低内存使用。
2.2、引擎等级
此等级需要OpenGL ES及游戏引擎高手。
2.3、C++语言等级
在这个等级中,建议编写无内存泄露的代码,遵循Cocos2d-x内置的内存管理原则,尽量避免内存泄露。
3、提示和技巧
- 一帧一帧载入游戏资源。
- 减少绘制调用,使用“Auto-batching”自动批处理。
- 载入纹理时按照从大到小的顺序。
- 避免高峰内存使用。
- 使用载入屏幕预载入游戏资源。
- 需要时释放空闲资源。
- 收到内存警告后释放缓存资源。
- 使用纹理打包器优化纹理大小、格式、颜色深度等。
- 使用JPG格式要谨慎。
- 请使用RGB4444颜色深度16位纹理。
- 请使用NPOT纹理,不要使用POT纹理。
- 避免载入超大纹理。
- 推荐1024 * 1024 NPOT
pvr.ccz纹理集,而不要采用RAW PNG纹理。