Cocos2d-x 3.2 的内存管理详解

时间:2022-09-07 10:50:14

目标读者:了解 Cocos2d-x 中的节点以及节点树,了解引用计数,了解游戏主循环等概念。


本文首先介绍 Cocos2d-x 3.2 中内存管理的作用,以及各个作用的应用。借由通俗易懂的解释来了解内存管理的过程。其次通过源码解析介绍其内部的实现原理。加深理解,从而在有需要的时候绕开引擎建立自己的内存管理机制。

一、Cocos2d-x 3.2 内存管理的两个方面


1) 通过加入 autorelease 来自动释放那些创建后未使用的对象。
2) 通过节点管理来保证对象在弃用后及时地删除。

1、及时释放弃用的对象

使用条件:该对象是Node的子类对象
使用方法:addChild、removeChild

内存管理过程:

addChild //添加对象后,对象可以被使用
removeChild //删除对象后,对象被立刻删除(通过 delete)

2、及时释放未使用的对象

简述新创建的对象如果一帧内不使用,就会被自动释放。(所谓一帧,即是一个gameloop。)
使用条件:对象通过CREAT_FUNC()宏创建或者对象使用autorelease加入了自动释放池。
使用方法:自动实现

内存管理过程:

  • 对象不使用的情况

    对象创建 引用+1
    对象自动释放 引用-1
  • 对象使用的情况

    对象创建 引用+1
    对象使用 引用+1 // 通过 addChild 使用对象
    对象自动释放 引用-1

引用的初始值为0,如果一帧结束后对象的引用值还是0,那就就会被 delete。



二、内存管理的实现原理

涉及内存管理的文件很多,仅展示直接相关的部分代码。

1、第一部分

Ref 类: 进行引用计数、提供加入自动释放池的接口。

AutoreleasePool 类: 管理一个 vector 数组来存放加入自动释放池的对象。提供对释放池的清空操作。

PoolManager 类: 管理一个 vector 数组来存放自动释放池。默认情况下引擎只创建一个自动释放池,因此这个类是提供给开发者使用的,例如出于性能考虑添加自己的自动释放池。

DisplayLinkDirector 类: 这是一个导演类,提供游戏的主循环,实现每一帧的资源释放。这个类的名字看起来有点怪,但是不用管它。因为这个类继承了 Director 类,也是唯一一个继承了 Director 的类,也就是说完全可以合并为一个类,引擎开发者在源码中有部分说明。

1.1 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

// 存放释放池对象的数组
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

// 释放池管理器单例对象
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

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、第二部分

Node 类:提供了 addChildremoveChild 方法来创建游戏的节点树。
Vector 类:封装了对于对象的 retain 操作和 release 操作。

2.1 Node

// 添加节点
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

// 这里仅展示与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;这个 mainLoopdrawScene() 完成后,一帧结束, Director 通过释放池将池中的对象 clear(),即对 Node 对象A进行 release 操作。A的引用计数变为0,执行 delete 释放A对象。

3、高阶用法

之所以称为高阶用法,是因为,如果开发者对 Cocos 的内存管理机制理解不够深刻,那么很可能会用错而导致损失大于收益。另一方面,这类用法在平时很少会用到。

3.1 使用 retain 来延长对象的生存时间

在开发过程中,如果需要使用一个节点对象,但是又不想把它放到节点树里面去。那么就可以使用 retain 来避免对象被自动释放掉。

3.2 使用 PoolManagerpush 来延长对象的生存时间

有些情况下,希望闲置对象晚一帧进行销毁,可以使用 push 把当前释放池推入栈底,那么这一帧结束的时候只会释放刚 push 进去的释放池。

笔者本身还没有机会使用过高阶用法,如果有小伙伴发现了高阶用法在实际问题中的应用,敬请留言交流。

四、参考链接

深入理解 Cocos2d-x 内存管理 (从这篇文章中获得许多启发。)
Cocos2d-x源码(位于Github上,如果连不上尝试使用前缀 https)
引用计数——*(关于引用计数的说明)
Cocos2d-x的内存管理机制概述(里面提到了为什么要有 PoolManager)
cocos2dx 3.2 (24)——内存管理机制(编纂地比较详细)