Cocos内存管理源码(autorelease解析)
背景
这段时间在做项目的时候,需求需要往spine动作的挂点上绑定按钮节点,由于按钮在编辑器中是加在已有节点上的,所以在往spine上添加挂点时,需要先移除按钮,然后再绑定的挂点上。
local spineAnim = sp.SkeltonAnimation:create(skeletonFile, atlasFile, 1.0, true)
local btnGame = roleNode:getChildByName("btnGame")
btnGame:removeFromParent()
spineAnim:addSlotBindInfo("qianwangduiju", btnGame, Defind.slotBindType.slotBindType_all)
如果直接这样写,会在某种情况导致btnGame按钮节点丢失,无法正常挂载再spine动画节点上,后续优化了此方案
local spineAnim = sp.SkeltonAnimation:create(skeletonFile, atlasFile, 1.0, true)
local btnGame = roleNode:getChildByName("btnGame")
btnGame:retain()
btnGame:removeFromParent()
btnGame:autorelease()
spineAnim:addSlotBindInfo("qianwangduiju", btnGame, Defind.slotBindType.slotBindType_all)
在移除按钮之前,先retain一下,这样引用计数加1,就不会导致内存被回收,再调用autorelease,此时并不会release对象,这时会将此节点加入_managedObjectArry对象池中,在Director的mainLoop中会调用PoolManager::getInstance()->getCurrentPool()->clear();
具体详情解析,请接着看
在cocos2dx-3.8
中的自动内存管理是用引用计数来实现的,对于老版本的coocs引用计数使用的是CCObejct
,但这个类后续被弃用了,使用CCRef
代替,cocos中几乎所有的类都是继承于CCRef
CCRef
基本原理就是其内部存在一个引用计数_referenceCount
,当这个计数为0时,就会被释放。引用计数通过retain
,release
操作。
Ref从创建到销毁的过程
举个栗子,向屏幕中添加一个Button
来测试Ref
的创建和销毁,首先创建一个Button
auto button = Button::create();
button->setName("myButton");
addChild(button);
以上代码就是向屏幕中添加一个button,让我们看看create
做了什么
Button* Button::create()
{
Button* widget = new (std::nothrow) Button();
if (widget && widget->init())
{
widget->autorelease();
return widget;
}
CC_SAFE_DELETE(widget);
return nullptr;
}
create
函数是一个工厂方法,cocos
中很多类都实现了这个方法,其中可以看到ret->autorelease();
,这个函数就是把当前对象加入到自动释放池内,对于自动释放池下面会详细讲解。(注:Ref
初始化的时候引用计数为1不是0)
接下来看下addChild()
接口,此处截取了一部分
void Node::addChild(Node *child){
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name);
}
void Node::addChild(Node* child, int localZOrder, const std::string &name){
...
addChildHelper(child, localZOrder, INVALID_TAG, name, false);
}
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag){
...
this->insertChild(child, localZOrder);
...
}
void Node::insertChild(Node* child, int z){
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
child->_localZOrder = z;
}
最终跳转到insertChild
中,通过 _children.pushBack
把 button
加入到_children
中去,到底引用计数在哪里+1操作?答案在pushBack
操作中,_children
是cocos
为Ref
量身定制的向量Vector<T>
,这个向量只能给继承Ref
的类使用
void pushBack(T object){
...
_data.push_back( object );
object->retain();
}
代码中可以看到object->retain()
,对添加进来的对象引用+1操作,那么什么时候-1呢?
当我们移除场景的时候,应该释放场景中的button的。Node
被移除时会调用当前的Node
的父亲的removeChild
函数,此函数最后会调用Node
的cleanup
函数,cleanup
函数时递归函数,会遍历所有子节点。当cleanup
完之后会从父节点的_children
这个向量中删除,此时就会调用release
函数
//当某个儿子节点cleanup完之后会调用_children.earse(childIndex)
iterator erase(ssize_t index){
...
auto it = std::next( begin(), index );
(*it)->release();
return _data.erase(it);
}
release
函数就是当前实例的引用计数-1,如果-1后为0那么释放内存
void Ref::release(){
...
--_referenceCount;
if (_referenceCount == 0){
...
delete this;
}
}
自动释放池
Ref
中的autorelease
函数,咋一看感觉内存不需要我来管了,他会自动释放。然而这个自动和我脑子里面的自动向差一个孙猴子的跟头,毕竟c++
不是java
,先看看autorelease
的源码
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
代码很短,autorelease
并没有release
,而是把对象加入到了对象池中。那么这个对象池是什么时候去release
里面的对象呢?接下来就要看Director
的mainLoop
函数了,这个函数在Director
中实现。
void Director::mainLoop()
{
if(_purgeDirectorInNextLoop)
{
...
}
else if(_restartDirectorInNextLoop)
{
...
}
else if(!_invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}
mainLoop
是每一帧调用的函数,我们发现cocos
在每一帧结束绘制drawScene
之后都会调用PoolManager::getInstance()->getCurrentPool()->clear();
的操作,接下来我们看看clear
的实现细节。
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release();
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}
我们发现,在clear
里面对所有在_managedObjectArray
中的所有对象都进行一次release
操作,并把它从_managedObjectArray
中删掉。_managedObjectArray
是什么,查看前一段代码中addObject
的实现细节就知道,autorelease
就是把当前对象加入到_managedObjectArray
中
也就是说,我们创建的Button
的时候引用计数为1,然后调用autorelease
添加到_managedObjectArray
中,之后又被addChild
到屏幕中,此时引用计数为2。当一帧绘制结束的时候会系统会调用释放池的clear
函数,此函数会遍历所有在自动释放池内的对象并release
,最后从对象池中删除之(所以第二帧结束后不会被再次调用release
了),此时引用计数为1。当我们把当前场景移除的时候会调用release
把引用计数减少至0,并从内存中释放。