Cocos2d-x中的引用计数(Reference Count)和自动释放池(AutoReleasePool)
引用计数
引用计数是C/C++项目中一种古老的内存管理方式。早在8年前我研究一款名为TCPMP的开源项目时,引用计数就已存在。
iOS SDK将这项技术封装到了NSAutoreleasePool中。在Cocos2d-x里,我们克隆了一套CCAutoreleasePool。两者的用法基本相同,若你未涉足iOS开发,可以查阅苹果官方文档NSAutoreleasePool Class Reference。
CCAutoreleasePool
Cocos2d-x的CCAutoreleasePool与Cocoa的NSAutoreleasePool概念和API相同,但有两点重要区别:
- 无法手动创建:Cocos2d-x会为每个游戏创建一个自动释放池实例对象,游戏开发者不能新建自动释放池,只需专注于
release/retaincocos2d::CCObject的对象。 - 不支持多线程:
CCAutoreleasePool不能用于多线程。若游戏需要网络线程,仅可在网络线程中接收数据、改变状态标志,切勿在该线程中调用Cocos2d接口。原因如下:
CCAutoreleasePool的逻辑是,当调用object->autorelease()时,对象会被放入自动释放池。自动释放池会维持对象的生命周期,直至当前消息循环结束。在消息循环的最后,若该对象未被其他类或容器retain过,它将自动释放。例如,layer->addChild(sprite)将sprite添加到layer的子节点列表中,sprite的生命周期会持续到layer释放时,而非在当前消息循环的最后被释放。
这就是不能在网络线程中管理CCObject生命周期的原因。因为在每个UI线程的最后,自动释放对象会被删除,调用这些已删除的对象时会导致程序崩溃。
CCObject::release()、retain() 和 autorelease()
简而言之,只有两种情况需要调用release()方法:
- 手动
new一个cocos2d::CCObject子类的对象,如CCSprite、CCLayer等。 - 获取
cocos2d::CCObject子类对象的指针,并在代码中调用过retain方法。
以下是不需要调用retain和release方法的例子:
CCSprite* sprite = CCSprite::create("player.png");
使用静态构造函数
CCSprite::create("player.png")是使用静态构造函数的示例。在Cocos2d-x中,除单例外的所有类都提供了静态构造函数,这些静态构造函数包含以下4项操作:
- 新建一个对象。
- 调用
object->init(…)。 - 若初始化成功(如成功找到纹理文件),则调用
object->autorelease()。 - 返回已标记为
autorelease的对象。
所有CCAsdf::createWithXxxx(…)类型的函数都遵循上述方式。
在Cocos2d-x v1.x或更早版本中,使用方式如下:
CCSprite* sprite = CCSprite::spriteWithTexture(…);
使用这些静态构造函数时,无需关注new、delete和autorelease,只需关注object->retain()和object->release()。
一个错误的例子
有开发者报告了一个使用CCArray导致崩溃的例子:
bool HelloWorld::init()
{
bool bRet = false;
do
{
// super init first
CC_BREAK_IF(! CCLayer::init());
// add your codes below…
CCSprite* bomb1 = CCSprite::create("CloseNormal.png");
CCSprite* bomb2 = CCSprite::create("CloseNormal.png");
CCSprite* bomb3 = CCSprite::create("CloseNormal.png");
CCSprite* bomb4 = CCSprite::create("CloseNormal.png");
CCSprite* bomb5 = CCSprite::create("CloseNormal.png");
CCSprite* bomb6 = CCSprite::create("CloseNormal.png");
addChild(bomb1, 1);
addChild(bomb2, 1);
addChild(bomb3, 1);
addChild(bomb4, 1);
addChild(bomb5, 1);
addChild(bomb6, 1);
m_pBombsDisplayed = CCArray::create(bomb1, bomb2, bomb3, bomb4, bomb5, bomb6, NULL);
// m_pBombsDisplayed 是在头文件中被定义为一个 protected 变量.
// <--- 我们应该添加在这里m_pBombsDisplayed->retain()方法来防止在HelloWorld::refreshData()中crash。
this->scheduleUpdate();
bRet = true;
} while (0);
return bRet;
}
void HelloWorld::update(ccTime dt)
{
refreshData();
}
void HelloWorld::refreshData()
{
m_pBombsDisplayed->objectAtIndex(0)->setPosition(ccp(100, 100));
}
该错误在于m_pBombsDisplayed是使用CCArray::create(…)创建的,这是静态构造方式,数组被标记为autorelease。因此,该数组会在当前消息循环的最后被CCAutoreleasePool释放。
当后续消息循环调用HelloWorld::update(ccTime)时,m_pBombsDisplayed已成为野指针,从而导致崩溃。
为修复此崩溃问题,需在m_pBombsDisplayed = CCArray::create(…);之后添加m_pBombsDisplayed->retain(),并在HelloWorld::~HelloWorld()的析构函数中调用m_pBombsDisplayed->release()。