最新文章
Cocos2d-x游戏开发实例详解7:对象释放时机
03-25 13:59
Cocos2d-x游戏开发实例详解6:自动释放池
03-25 13:55
Cocos2d-x游戏开发实例详解5:神奇的自动释放
03-25 13:49
Cocos2d-x游戏开发实例详解4:游戏主循环
03-25 13:44
Cocos2d-x游戏开发实例详解3:无限滚动地图
03-25 13:37
Cocos2d-x游戏开发实例详解2:开始菜单续
03-25 13:32
Cocos2d-x 3.x基础学习:内存管理详解
在之前的基础学习教程中,我们对内存管理机制已有过介绍。本文将再次详细探讨内存管理问题,首先介绍Cocos2d-x 3.2中内存管理的作用及其应用,通过通俗易懂的解释让大家了解内存管理的过程;其次,通过源码解析介绍其内部的实现原理,加深大家的理解,以便在有需要的时候绕开引擎建立自己的内存管理机制。
一、Cocos2d-x 3.2内存管理的两个方面
1. 及时释放弃用的对象
- 使用条件:该对象是Node的子类对象。
- 使用方法:addChild、removeChild。
- 内存管理过程:
- addChild:添加对象后,对象可以被使用。
- removeChild:删除对象后,对象会立刻被删除(通过delete)。
2. 及时释放未使用的对象
- 简述:新创建的对象如果在一帧内不使用,就会被自动释放。这里的一帧指的是一个gameloop。
- 使用条件:对象通过CREAT_FUNC()宏创建或者对象使用autorelease加入了自动释放池。
- 使用方法:自动实现。
- 内存管理过程:
- 对象不使用的情况:
- 对象创建:引用 +1。
- 对象自动释放:引用 -1。
- 对象使用的情况:
- 对象创建:引用 +1。
- 对象使用:引用 +1(通过addChild使用对象)。
- 对象自动释放:引用 -1。
引用的初始值为0,如果一帧结束后对象的引用值还是0,该对象就会被delete。
二、内存管理的实现原理
涉及内存管理的文件众多,这里仅展示直接相关的部分代码。
1. 第一部分
1.1 Ref类
Ref类主要进行引用计数,并提供加入自动释放池的接口。以下是相关代码:
// 引用计数变量
unsigned int _referenceCount;
// 对象被构造后,引用计数值为 1
Ref::Ref()
: _referenceCount(1) // 当Ref对象被创建时,引用计数的值为 1
{
#if CC_ENABLE_SCRIPT_BINDING
static unsigned int uObjectCount = 0;
_luaID = 0;
_ID = ++uObjectCount;
_scriptObject = nullptr;
#endif
#if CC_USE_MEM_LEAK_DETECTION
trackRef(this);
#endif
}
// 引用 +1
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
++_referenceCount;
}
// 引用 -1 。如果引用为0则释放对象
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
#if CC_USE_MEM_LEAK_DETECTION
untrackRef(this);
#endif
delete this; // 注意这里把对象 delete 了
}
}
// 提供加入自动释放池的接口。对象调用此函数即可加入自动释放池的管理。
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
// 获取引用计数值
unsigned int Ref::getReferenceCount() const
{
return _referenceCount;
}
1.2 AutoreleasePool类
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;
}
1.3 PoolManager类
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();
}
1.4 DisplayLinkDirector类
DisplayLinkDirector类是一个导演类,提供游戏的主循环,实现每一帧的资源释放。该类继承了Director类,且是唯一一个继承了Director的类,名字看起来有点怪,但不用在意。代码如下:
void DisplayLinkDirector::mainLoop()
{
// 第一次当导演
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector(); // 进行清理工作
}
else if (! _invalid)
{
// 绘制场景,游戏主要工作都在这里完成
drawScene();
// 清空资源池
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
根据目前的分析,我们来梳理一下内存管理的过程:首先,创建一个Node对象A,由于Node继承Ref,所以Ref的引用计数为1;然后,A通过autorelease将自己放入自动释放池;drawScene()完成后,一帧结束,Director通过释放池将池中的对象clear(),即对Node对象A进行release()操作,A的引用计数变为0,执行delete释放A对象。
2. 第二部分
2.1 Node类
Node类提供了addChild和removeChild方法来创建游戏的节点树。代码如下:
// 添加节点
void Node::addChild(Node *child)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name);
// 经过这个方法-->addChildHelper-->insertChild,完成retain操作
}
// 移除节点
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
if (_children.empty())
{
return;
}
ssize_t index = _children.getIndex(child);
if (index != CC_INVALID_INDEX)
this->detachChild( child, index, cleanup );
// 注意这个函数
}
// 插入节点
void Node::insertChild(Node* child, int z)
{
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
// pushBack方法对节点进行了retain
child->_setLocalZOrder(z);
}
// 剥离节点
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
...
// 部分省略
_children.erase(childIndex);
// erase方法对节点进行了release
}
2.2 Vector类
Vector类封装了对于对象的retain操作和release操作。这里仅展示与Node类相关的内存管理的部分代码:
// 将对象入栈,引用 +1
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain(); // 进行了retain
}
// 将目标位置的对象移除
iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index );
(*it)->release(); // 进行了release
return _data.erase(it);
}
至此,我们可以完整地讲述内存管理的过程:首先,创建一个Node对象A,Node继承Ref,Ref的引用计数为1;然后A通过autorelease将自己放入自动释放池;接着,有一个Node对象B,B通过addChild(A)使得A的引用 +1;几个mainLoop后,B通过removeChild(A)使得A的引用 -1;这个mainLoop的drawScene()完成后,一帧结束,Director通过释放池将池中的对象clear(),即对Node对象A进行release操作,A的引用计数变为0,执行delete释放A对象。
3. 高阶用法
之所以称为高阶用法,是因为如果开发者对Cocos的内存管理机制理解不够深刻,很可能会用错而导致损失大于收益。而且这类用法在平时很少会用到。
3.1 使用retain来延长对象的生存时间
在开发过程中,如果需要使用一个节点对象,但又不想把它放到节点树里面去,那么可以使用retain来避免对象被自动释放。
3.2 使用PoolManager的push来延长对象的生存时间
有些情况下,希望闲置对象晚一帧进行销毁,可以使用push把当前释放池推入栈底,这样这一帧结束的时候只会释放刚push进去的释放池。
笔者本身还没有机会使用过高阶用法,如果有小伙伴发现了高阶用法在实际问题中的应用,敬请留言交流。