Cocos2d-x 的内存管理方法基于 Cocos2d-iPhone 的 Objective-C 风格的内存管理,把 Objective-C 的内存管理方式引入 C++。
1 复杂的内存管理
C++设计上不包含任何智能管理内存的机制,所以开发过程中需要考虑如何管理内存,包括:
- 内存分配与回收的方式和时机
- 针对堆和栈做不同的优化处理
- 过于零散的对象分配回收可能导致堆中的内存碎片化,降低内存的使用效率
- 等等
内存管理的核心是动态分配的对象必须保证在使用完毕后有效地释放内存,即管理对象的生命周期。
2 现有的智能内存管理技术
目前,主要有两种实现智能管理内存的技术,一是引用计数,一是垃圾回收。
-
引用计数:它通过给每个对象维护一个引用计数器,记录该对象当前被引用的次数。当对象增加一次引用时,计数器加 1;而对象失去一次引用 时,计数器减 1;当引用计数为 0 时,标志着该对象的生命周期结束,自动触发对象的回收释放。
引用计数的重要规则是每一个程序片段必须负责任地维护引用计数,在需要维持对象生存的程序段的开始和结束分别增加和减少一次引用计数,这样我们就可以实现十分灵活的智能内存管理了。
实际上,这与 new 和 delete 的配对使用十分类似,但是很巧妙地将生成和回收的事件转换成了使用和使用结束的事件。
引用计数解决了对象的生命周期管理问题,但堆碎片化和管理烦琐的问题仍然存在。
-
垃圾回收:它通过引入一种自动的内存回收器,它会自动跟踪每一个对象的所有引用,以便找到所有正在使用的对象,然后释放其余不再需要的对象。
垃圾回收器还可以压缩使用中的内存,以缩小堆所需要的工作空间。垃圾回收可以防止内存泄露,有效地使用可用内存。但是,垃圾回收器通常是作为一个单独的低级别的线程运行的,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用过的对象进行清除和回收,不能手动指派垃圾回收器回收某个对象。
回收机制包括分代复制垃圾回收、标记垃圾回收和增量垃圾回收。
3 Cocos2d-x的内存管理机制
Cocos2d-x采用了引用计数与自动回收的内存管理机制。
- 为了实现对象的引用计数,封装了根类CCRef,其它所有类都派生自CCRef。
/**
* Retains the ownership.
*
* This increases the Ref's reference count.
*
* @see release, autorelease
* @js NA
*/
void retain();
/**
* Releases the ownership immediately.
*
* This decrements the Ref's reference count.
*
* If the reference count reaches 0 after the decrement, this Ref is
* destructed.
*
* @see retain, autorelease
* @js NA
*/
void release();
/**
* Releases the ownership sometime soon automatically.
*
* This decrements the Ref's reference count at the end of current
* autorelease pool block.
*
* If the reference count reaches 0 after the decrement, this Ref is
* destructed.
*
* @returns The Ref itself.
*
* @see AutoreleasePool, retain, release
* @js NA
* @lua NA
*/
Ref* autorelease();
/**
* Returns the Ref's current reference count.
*
* @returns The Ref's reference count.
* @js NA
*/
unsigned int getReferenceCount() const;
retain() 引用计数增 1,获取该对象的引用权
release() 令其引用计数值减 1,释放该对象的引用权
autorelease() 将对象放入当前自动回收池(PoolManager::getInstance()->getCurrentPool()),当回收池自身被释放 的时候,它就会对池中的所有对象执行一次 release()方法,实现灵活的垃圾回收。
getReferenceCount() 生命周期的引用计数值
为了达到对象的自动回收,实现了CCPoolManager来管理AutoreleasePool对象池。
PoolManager* PoolManager::getInstance()
{
if (s_singleInstance == nullptr)
{
s_singleInstance = new (std::nothrow) PoolManager();
// Add the first auto release pool
new AutoreleasePool("cocos2d autorelease pool");
}
return s_singleInstance;
}
AutoreleasePool::AutoreleasePool(const std::string &name)
: _name(name)
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}
- 每次主循环会释放回收池PoolManager::getInstance()->getCurrentPool()->clear(),在下一帧开始前,引用计数值减 1,释放掉不在使用的对象。
void Director::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
4 工厂方法
工厂方法是程序设计中一个经典的设计模式,指的是基类中只定义创建对象的接口,将实际的实现推迟到子类中。在这里, 我们将它稍加推广,泛指一切生成并返回一个对象的静态函数。一个经典的工厂方法如同这样:
CCObject* factoryMethod()
{
CCObject* ret = new CCObject();
}
//在这里对 ret 对象进行必要的初始化操作 return ret;
这段看起来正常的代码其实隐藏着一个问题:工厂方法对ret对象的引用在函数返回时已经结束,但是它没有释放对ret 的引用,埋下了内存泄露的隐患。但是,如果在函数返回前就执行 release(),这显然是不合适的,因为这会触发对象的回 收,再返回的对象指针就成为了错误指针。
autorelease()方法很好地解决了回收池中的对象不会在使用完毕前释放。利用 autorelease()修改后的工厂方法如下:
CCObject* factoryMethod()
{
CCObject* ret = new CCObject(); //这里对 ret 对象进行必要的初始化操作
ret->autorelease();
return ret;
}
5 关于对象传值
对象赋值给某一指针作为引用的时,需要获得新对象的引用权,释放旧对象的引用权。此时,release()和 retain()的顺序是尤为重要的。首先来看下面一段代码:
void SomeClass::setObject(CCObject* other)
{
this->object->release();
other->retain();
this->object = other;
}
6 释放:release()还是autorelease()?
autorelease()的垃圾池机制需要占用内存和CPU资源,每次执行autorelease() 的过程,实际上对应的是执行成对的 retain()和 release(),以及一次成对的容器存取,还包括其他的逻辑判断。
过多不必要的autorelease()将导致垃圾池臃肿,在存在大量内存操作的程序中尤为严重
autorelease()只有在自动释放池被释放时才会进行一次释放操作,如果对象释放的次数超过了应有的次数,则这个错误在调用 autorelease()时并不会被发现,只有当自动释放池被释放时(通常也就是游戏的每一帧结束时),游戏才会崩溃。