本文适用于cocos2dxV3.17.2 - V4.0版本 (2019.5至今)
核心基类-Ref
-
在cocos/base cocos/2d 等核心类都是Ref的派生类,而Ref类正是充当了内存管理的角色
-
Ref 定义
class CC_DLL Ref
{
public:
void retain();
void release();
Ref* autorelease();
protected:
Ref();
protected:
unsigned int _referenceCount; // 引用计数
friend class AutoreleasePool;
- 当Ref 被创建的时候,对象引用计数为1
Ref::Ref()
: _referenceCount(1)
}
- 引用计数+1
void Ref::retain()
{
++_referenceCount;
}
- 引用计数-1, 当引用计数为0时,delete 对象
void Ref::release()
{
--_referenceCount;
if (_referenceCount == 0)
{
delete this;
}
}
- 核心:autorelease,将自身添加至PoolManager当前自动释放池(AutoreleasePool),将自身交给PoolManager管理
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
自动释放池类-AutoreleasePool
- AutoreleasePool 定义
class CC_DLL AutoreleasePool
{
public:
AutoreleasePool(const std::string &name);
~AutoreleasePool();
void addObject(Ref *object);
void clear();
private:
std::vector<Ref*> _managedObjectArray; // 保存池内管理的对象,(池全部对象)
std::string _name; // 池的名字,cocos维护的池有一个名字,你可以用其他名字创建自己的池
- AutoreleasePool 构造函数将为 _managedObjectArray 申请预留空间150 ,然后将自身push到PoolManager实例
AutoreleasePool::AutoreleasePool(const std::string &name)
: _name(name)
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}
- 向池末增加对象
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object);
}
- 池内全部对象都执行一次Ref::release(), 然后清空_managedObjectArray(也就清空了池)
void AutoreleasePool::clear()
{
std::vector<Ref*> releasings; // 空vector
releasings.swap(_managedObjectArray); // 互换后,_managedObjectArray就是空的vector
for (const auto &obj : releasings)
{
obj->release();
}
// 局部变量releasings被销毁
}
自动释放池管理类-PoolManager
- 该类的作用是管理AutoreleasePool
- PoolManager 定义
class CC_DLL PoolManager
{
public:
static PoolManager* getInstance();
static void destroyInstance();
AutoreleasePool *getCurrentPool() const;
bool isObjectInPools(Ref* obj) const;
friend class AutoreleasePool;
private:
PoolManager();
~PoolManager();
void push(AutoreleasePool *pool);
void pop();
static PoolManager* s_singleInstance; // 单例
std::vector<AutoreleasePool*> _releasePoolStack; // 用来保存被PoolManager管理的AutoreleasePool
- 获取PoolManager实例对象
PoolManager* PoolManager::getInstance()
{
if (s_singleInstance == nullptr)
{
s_singleInstance = new (std::nothrow) PoolManager(); // 实例化PoolManager单例
new AutoreleasePool("cocos2d autorelease pool"); // 创建自动释放池AutoreleasePool,Application生命周期内cocos只维护该池
}
return s_singleInstance;
}
- 销毁 PoolManager实例对象
- 销毁时机是Application被销毁时,(调用Application基类 ApplicationProtocol::~ApplicationProtocol())
void PoolManager::destroyInstance()
{
delete s_singleInstance;
s_singleInstance = nullptr;
}
- 获取当前AutoreleasePool
AutoreleasePool* PoolManager::getCurrentPool() const
{
return _releasePoolStack.back(); // 读取Vector末尾值,除非用户有自定义AutoreleasePool,否则Vector只有唯一元素
}
- PoolManager._releasePoolStack末尾增加池
- PoolManager._releasePoolStack删除末尾池
void PoolManager::push(AutoreleasePool *pool)
{
_releasePoolStack.push_back(pool);
}
void PoolManager::pop()
{
_releasePoolStack.pop_back();
}
主循环
- 在导演的主循环中,每帧(每此执行mainLoop)渲染场景后, 进行了一次AutoreleasePool::clear()
void Director::mainLoop()
{
// 前面处理一些导演清空或重置的情况
// 下面才是我们需要关注
else if (! _invalid)
{
drawScene(); // 渲染场景
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
小结
根据上文我们知道,创建Ref派生类实例对象的时候,构造函数会设置实例对象的引用计数为1;
而每帧渲染过后,PoolManager会执行AutoreleasePool::clear() ,先将池内对象引用计数-1, 然后清空池;
假设代码是这样写的:
auto obj= new Node(); // obj引用计数为1
while(1) // 主循环
{
drawScene();
PoolManager::getInstance()->getCurrentPool()->clear(); // obj引用计数将被减一,变为0,且被delete
}
也就是说上一帧New出来的对象,他在下一帧渲染后就会被销毁,他的生命周期只有这么短吗,如果这么短为什么我们看到游戏界面的UI能一直存在?
想知道问题的答案请往下阅读文章。
Node 类
- Node保存子节点的成员变量:Vector<Node*> _children;
- 凡是继承了Node的节点都有addChild、removeChild
// addChild 跟踪
void Node::addChild(Node* child, int localZOrder, const std::string &name)
{
addChildHelper(child, localZOrder, INVALID_TAG, name, false); // Add1
}
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
this->insertChild(child, localZOrder); // Add2
if (setTag)
child->setTag(tag);
else
child->setName(name);
child->setParent(this);
}
void Node::insertChild(Node* child, int z)
{
_children.pushBack(child); // Add3
}
// -------------------------------------
// removeChild 跟踪
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
ssize_t index = _children.getIndex(child);
if( index != CC_INVALID_INDEX )
this->detachChild( child, index, cleanup ); // Remove1
}
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
child->setParent(nullptr);
_children.erase(childIndex); // Remove1
}
Vector 类
- Node::addChild >>> Vector::pushBack
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain(); // Add Finish
}
- Node::removeChild >>> Vector::pushBack
void pushBack(const Vector<T>& other)
{
for(const auto &obj : other) {
_data.push_back(obj);
obj->retain(); // Remove Finish
}
}
- Vector析构函数
~Vector<T>()
{
CCLOGINFO("In the destructor of Vector.");
clear();
}
void clear()
{
for( auto& it : _data) {
it->release(); // 好好留意这句:当Vector析构的时候,会对所有元素进行release(对应本文最后一句)
}
_data.clear();
}
小结
根据上文我们知道,创建Ref派生类实例对象子节点A的时候,构造函数会设置实例对象的引用计数为1;
然后父节点会使用addChild,将子节点A保存到自身的vector属性内,而且AddChild也会调用Ref::retain 使子节点A引用计数+1
而每帧渲染过后,PoolManager会执行AutoreleasePool::clear() ,先将池内对象引用计数-1, 然后清空池;
代码就变成了这样:
auto director = Director::getInstance(); // 单例
auto scene = WelcomeScene::createScene(); // createScene等同create, create调用了autorelease; scene引用计数为1
auto sprite = Sprite::create("HelloWorld.png"); // create 里调用了autorelease; sprite引用计数为1
scene.addChild(sprite) // sprite 引用计数为2
director->runWithScene(scene); // 同理通过director._scenesStack.pushBack(scene); 将scene引用+1,变为2
while(1) // director:: mainloop主循环
{
drawScene();
// sprite引用计数将被减1,变为1;
// 而且清空了AutoreleasePool,sprite不再被PoolManager管理,即下一帧不会再减1;
// 直到游戏执行了逻辑scene.removeChild(sprite)后,引用计数变为0,sprite才被delete
// scene 类似于 sprite:Director::popToRootScene >>> director._scenesStack.popBack();
PoolManager::getInstance()->getCurrentPool()->clear();
}
总结
- Ref 是cocos 节点对象基类,当Ref对象构造后,引用计数为1
- 使用Ref::retain() 使引用计数+1
- 使用Ref::release() 使引用计数-1;当引用计数为0的时候,对象会被delete;
- 使用Ref::autorelease() 将对象交给 PoolManager 管理
- 每帧渲染后,PoolManager会清空池,而且会对清空前池内的所有对象进行Ref::release(),销毁无用对象;
- 清空池后,PoolManager 管理的池是全新的,空的,直到下一帧有新的对象调用了Ref::autorelease();
- addChild 将调用Ref::retain()
- removeChild 将调用Ref::release()
obj = New Node(); // obj 引用计数为1
parent.addChild(obj) // obj 引用计数+1 变为2
// A帧
// obj 被PoolManager管理
drawScene();
PoolManager::getInstance()->getCurrentPool()->clear(); // obj 引用计数-1 变为1; PoolManager 管理的当前池被清空;
// A+1帧
// obj 已经没有被PoolManager管理
drawScene();
PoolManager::getInstance()->getCurrentPool()->clear();
// A++++++++...帧
parent.removeChild(obj) // obj 引用计数-1, 变为0 , 执行delete obj
用人话总结
- cocos2dx采用引用计数来管理内存,对象每次retain时引用计数+1,每次release时引用计数-1,当引用计数为0时,delete释放该对象。
- 继承于Ref的类在创建对象的时候都会适用自动引用计数方法autorelease(将引用计数初始化为1),将创建的对象放入内存管理池中。
- 在下一帧场景渲染后就释放掉这个内存管理池,此时该被添加到内存管理池中的对象就会全部release一次(引用计数-1,引用计数为0就释放)。
- 所以一个Ref派生类对象创建出来时,如果没有调用自身retain方法或者被其他对象管理(如addChild),就会在创建出来的下一帧释放掉。
- addChild方法中其实也是使用retain方法,removeChild的时候适用release方法。
- 父节点析构时也会调用子节点的release方法:node中保存子节点的数据结构为Vector<Node*> _children;在node析构后会调用_children的析构,在_children的析构函数中会遍历自己所有成员,并且调用release。