Cocos2d-x 3.x内存管理机制
1:C++内存管理
1-1:内存分配区域
创建对象需要两个步骤:第一步,为对象分配内存;第二步,调用构造函数初始化内存。在第一步中,可以选择几个不同的分配区域。这几个区域如下:
(1) 栈区域分配。栈内存分配运算内置于处理器的指令集中,效率很髙,但是分配的内 存容量有限。由处理器自动分配和释放,用来存放函数的参数值和局部变量的值等。在执 行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
(2) 堆区域分配。从堆上分配,也称动态内存分配。由开发人员分配释放,如果不释 放,程序结束时由操作系统回收。程序在运行时用malloc或new申请任意多少的内存,开发人员自己负责在何时用free或delete释放内存。动态内存的生存期由开发人员决定,使用非常灵活,但问题也最多。
(3) 在静态存储区域分配。这个内存空间在程序的整个运行期间都存在,内存在程序编译的时候就已经分配好。它可以分配全局变量和静态变量。
1-2:动态内存分配
动态内存分配最为灵活但是问题也很多,这里重点介绍动态内存分配。动态内存使用 malloc或new分配内存,使用free或delete释放内存。其中,malloc和free是成对的,new 和delete是成对的。
(1)malloc和free的使用
malloc和free是C/C++语言的标准库函数,主要是在C中使用。使用malloc创建对 象,不会自动调用构造函数初始化内存。使用free释放对象,不会自动调用析构函数清除内存。
(2)new和delete的使用
与malloc和free不同,new和delete不是函数库,而是C++的运算符。new运算符能 够完成创建对象所有步骤(即第一步,为对象分配内存;第二步,调用构造函数初始化内 存),它也会调用构造函数。实例代码如下:
MyObject * obj = new MyObject();
构造函数可以重载,根据用户传递的参数列表,决定调用哪个构造函数进行初始化对象。
new运算符的反操作运算符是delete,delete先调用析构函数,再释放内存。实例代码如下:
delete obj;
其中,obj是对象指针,obj只能释放new创建的对象,不能释放由malloc创建的。而且采 用delete释放后的对象指针,需要obj = NULL以防止“野指针”。
提示:一种情况是,指针变量没有被初始化,它的指向是随机的,它会乱指一气,并不是NULL。如果使用if语句判断,则认为是有效指针。另一种情况是,指针变量被free或者 delete之后,它们只是把指针所指的内存释放掉,但并没有把指针本身清除,此时指针指向的就是“垃圾”内存。如果使用if语句判断,也会认为是有效指针。“野指针”是很危险的,良好的编程习惯是,这两种情况下都需要将指针设置为NULL。这是避免“野指针”的唯一方法。
2:Cocos2d-x内存管理
在3.x版本,Cocos2d-x采用全新的根类Ref ,实现Cocos2d-x类对象的引用计数记录。引擎中的所有类都派生自Ref。Cocos2d-x内存管理是建立在C++语言new/delete之上,通过引入Object-C语言的引用计数来实现的。
2-1:内存引用计数
Ref类设计来源于Cocos2d-iphone的CCObject 类,在Cocos2d-x 2.x中也叫CCObject类。因此Ref类的内存管理是参考Objectives手动管理引用计数(reference count,RC)而设计的。
每个Ref对象都有一个内部计数器,这个计数器跟踪对象的引用次数,被称为“引用计数”(RC)。当对象被创建时,引用计数为1。为了保证对象的存在,可以调用retain函数保 持对象,retain会使其引用计数加1,如果不需要这个对象可以调用release函数,release使其引用计数减1。当对象的引用计数为0时,引擎就知道不再需要这个对象了,就会通过delete释放对象内存。
核心类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;
}
}
Cocos2d-x提供引用计数管理内存的方法如下:
- 调用 retain()方法:令其引用计数增1,表示获取该对象的引用权。
- 调用 release()方法:在引用结束的时候,令其引用计数值减1,表示释放该对象的引用权。
- 调用 autorelease()方法 :将对象放入自动释放池。当释放池自身被释放的时候,它就会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。Cocos2d-x提供AutoreleasePool,管理自动释放对象。当释放池自身被释放的时候,它就会对池中的所有对象执行一次release()方法。
Ref原理分析:
- 当一个Ref初始化(被new出来时),_referenceCount = 1;
- 当调用该Ref的retain()方法时,_referenceCount++;
- 当调用该Ref的release()方法时,_referenceCount–;
- 若_referenceCount减后为0,则delete该Ref。
2-2:autorelease使用
当一个继承自Ref的obj对象创建后,其引用计数_referenceCount为1;执行一次autorelease()后,obj对象被加入到当前的自动释放池AutoreleasePool,它能够管理即将释放的对象池。obj对象的引用计数值并没有减1。但是在一帧结束时刻或者称一个消息循环结束时刻,当前的自动释放池会被回收掉,并对自动释放池中的所有对象执行一次release()操作,当对象的引用计数为0时,对象会被释放掉。
所谓所谓一帧或者一个消息循环,即是一个gameloop(在导演类中)。每次为了处理新的事件,Cocos2d-x引擎都会创建一个新的自动释放池,事件处理完成后,就会销毁这个池,池中对象的引用计数会减1,如果这个引用计数会减0,也就是没有被其它类或Ref对象retain,则释放对象,否则这个对象不会释放,在这次销毁池过程中“幸存”下来,它被转移到下一个池中继续生存。总结起来就是:
- 系统在每一帧结束时都会销毁当前自动释放池,并创建一个新的自动释放池;
- 自动释放池在销毁时会对池中的所有对象执行一次release()操作;
- 新创建的对象如果一帧内不使用,就会被自动释放;
2-2-1 autorelease源码如下:
Ref* Ref::autorelease()
{
// 将节点加入自动释放池
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
2-2-2 与autorelease相关的类如下:
(1)AutoreleasePool类: 管理一个 vector 数组来存放加入自动释放池的对象。提供对释放池的清空操作。
// 存放释放池对象的数组
std::vector<Ref*> _managedObjectArray;
// 往释放池添加对象
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object);
}
// 清空释放池,将其中的所有对象都 delete
void AutoreleasePool::clear()
{
// 释放所有对象
for (const auto &obj : _managedObjectArray)
{
obj->release();
}
// 清空vector数组
_managedObjectArray.clear();
}
// 查看某个对象是否在释放池中
bool AutoreleasePool::contains(Ref* object) const
{
for (const auto& obj : _managedObjectArray)
{
if (obj == object)
return true;
}
return false;
}
(2)PoolManager 类: 管理一个 vector 数组来存放自动释放池。默认情况下引擎只创建一个自动释放池,因此这个类是提供给开发者使用的,例如出于性能考虑添加自己的自动释放池。
// 释放池管理器单例对象
static PoolManager* s_singleInstance;
// 释放池数组
std::vector<AutoreleasePool*> _releasePoolStack;
// 获取 释放池管理器的单例
PoolManager* PoolManager::getInstance()
{
if (s_singleInstance == nullptr)
{
// 新建一个管理器对象
s_singleInstance = new PoolManager();
// 添加一个自动释放池
new AutoreleasePool("cocos2d autorelease pool");// 内部使用了释放池管理器的push,这里的调用很微妙,读者可以动手看一看
}
return s_singleInstance;
}
// 获取当前的释放池
AutoreleasePool* PoolManager::getCurrentPool() const
{
return _releasePoolStack.back();
}
// 查看对象是否在某个释放池内
bool PoolManager::isObjectInPools(Ref* obj) const
{
for (const auto& pool : _releasePoolStack)
{
if (pool->contains(obj))
return true;
}
return false;
}
// 添加释放池对象
void PoolManager::push(AutoreleasePool *pool)
{
_releasePoolStack.push_back(pool);
}
// 释放池对象出栈
void PoolManager::pop()
{
CC_ASSERT(!_releasePoolStack.empty());
_releasePoolStack.pop_back();
}
我们可以自己创建AutoreleasePool,管理对象的autorelease。
我们已经知道,调用了autorelease()方法的对象(下面简称”autorelease对象”),将会在自动释放池释放的时候被释放一次。虽然,Cocos2d-x已经保证每一帧结束后释放一次释放池,并在下一帧开始前创建一个新的释放池,但是我们也应该考虑到释放池本身维护着一个将要执行释放操作的对象列表,如果在一帧之内生成了大量的autorelease对象,将会导致释放池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,我们最好可以手动创建并释放一个回收池。
(3)DisplayLinkDirector 类: 这是一个导演类,提供游戏的主循环,实现每一帧的资源释放。这个类的名字看起来有点怪,但是不用管它。因为这个类继承了 Director 类,也是唯一一个继承了 Director 的类,也就是说完全可以合并为一个类,引擎开发者在源码中有部分说明。
void DisplayLinkDirector::mainLoop()
{
//第一次当导演
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();//进行清理工作
}
else if (! _invalid)
{
// 绘制场景,游戏主要工作都在这里完成
drawScene();
// 清空资源池
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
2-2-3 总结:
- autorelease()的实质是将对象加入自动释放池,对象的引用计数不会立刻减1,在自动释放池被回收时对象执行release()。
- autorelease()只有在自动释放池被释放时才会进行一次释放操作,如果对象释放的次数超过了应有的次数,则这个错误在调用autorelease()时并不会被发现,只有当自动释放池被释放时(通常也就是游戏的每一帧结束时),游戏才会崩溃。在这种情况下,定位错误就变得十分困难了。例如,在游戏中,一个对象含有1个引用计数,但是却被调用了两次autorelease()。在第二次调用autorelease()时,游戏会继续执行这一帧,结束游戏时才会崩溃,很难及时找到出错的地点。因此,我们建议在开发过程中应该避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量以release()来释放对象引用。
- autorelease()并不是毫无代价的,其背后的释放池机制同样需要占用内存和CPU资源。过多的使用autorelease()会增加自动释放池的管理和释放池维护对象存取释放的支出。在内存和CPU资源本就不足的程序中使得系统资源更加紧张。此时就需要我们合理创建自动释放池管理对象autorelease。
- 不用的对象推荐使用release()来释放对象引用,立即回收。
3:Ref特殊内存管理
(1)Node的addChild/removeChild方法
在Cocos2d-x中,所有继承自Node类,在调用addChild方法添加子节点时,子节点会自动调用了retain。 对应的通过removeChild,移除子节点时,子节点会自动调用了release。
调用addChild方法添加子节点,节点对象执行retain。子节点被加入到节点容器中,父节点销毁时,会销毁节点容器释放子节点,即对子节点执行release。如果想提前移除子节点我们可以调用removeChild。
在Cocos2d-x内存管理中,大部分情况下我们通过调用addChild/removeChild的方式自动完成了retain,release调用。不需再调用retain,release。
(2)树形结构和链式反应
我们当前运行这一个场景,场景初始化,添加了很多层,层里面有其它的层或者精灵,而这些都是CCNode节点,以场景为根,形成一个树形结构,场景初始化之后(一帧之后),这些节点将完全 依附(内部通过retain)在这个树形结构之上,全权交由树来管理,当我们砍去一个树枝,或者将树连根拔起,那么在它之上的“子节点”也会跟着去除(内部通过release),这便是链式反应。
(3)工厂方法
在Cocos2d-x中,提供了大量的工厂方法create静态函数创建对象。仔细看你会发现,这些对象都是自动释放的。
#define CREATE_FUNC(__TYPE__)
static __TYPE__* create()
{
__TYPE__ *pRet = new __TYPE__();
if (pRet && pRet->init())
{
pRet->autorelease();
return pRet;
}
else
{
delete pRet;
pRet = NULL;
return NULL;
}
}
下面以一段代码说明Cocos2d-x对象自动释放过程。
void HelloWorld::test()
{
auto lable = Lable::create();
this->addChild(lable);
}
- 首先通过create方法创建了一个lable(创建过程中执行lable->autorelease()),对象被加入到当前的自动释放池AutoreleasePool中,此时label的引用计数为1;
- 执行this->addChild(lable)将label作为子节点加入Layer父节点中,此时label的引用计数为2;
- 当一帧结束后,当前的自动释放池AutoreleasePool被释放,对label执行了一次release操作,label的引用计数变为1,当前的自动释放池被销毁,lable->autorelease()的作用结束;
- 当游戏结束或者是切换场景时,label的父节点Layer会被销毁,发生链式反应,Layer对其子节点label执行release操作,label的引用计数变为0,执行delete label操作,内存得以回收。
4:内存管理使用建议
(1)类内部使用new分配的内存,在析构函数中执行delete操作;
(2)cocos2d-x的类元素通过create静态工厂创建,其内存会通过autorelease自动管理,无需其他操作;
(3)每个retain函数一定要对应一个release函数或一个autorelease函数;
(4)单例模式下由于只有一个实例,可以写一个成员函数用于delete对象指针,在游戏结束时执行该成员函数就可以了;