COCOS学习笔记--Cocod2dx内存管理(三)-Coco2d-x内存执行原理

时间:2022-03-14 00:22:44

通过上两篇博客。我们对Cocos引用计数和Ref类、PoolManager类以及AutoreleasePool类已有所了解,那么接下来就通过举栗子来进一步看看Coco2d-x内存执行原理是如何的。

 //先建一个node
Node * node = Node::create();
//创建完之后打印node的引用计数
schedule([node](float f){
//获得node的引用计数
int count = node->getReferenceCount();
//打印node的引用计数
log("node's ReferenceCount = %d",count);
},"test");

打印结果例如以下:

COCOS学习笔记--Cocod2dx内存管理(三)-Coco2d-x内存执行原理

能够看到引用计数打印出来是一大串的整数,事实上这时node已经被释放掉了是不存在的。引用计数为0,所以系统随便打印了一堆整数来表示。

我们先看一下Node类的源代码,其create()方法例如以下:

Node * Node::create()
{
Node * ret = new (std::nothrow) Node();
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}

看到这句Node * ret = new (std::nothrow) Node();首先new了一个node,我们知道Node是继承自Ref的。通过new创建对象就会调用其构造函数,而之前我们在Ref类的解说中知道:调用Ref构造函数中会运行这句_referenceCount(1)对其引用计数+1,所以node对象起始的引用计数为1。

我们还能够看到这句:ret->autorelease(),仅仅要通过create创建的node都会被加入到自己主动释放池中,我们在看下autorelease()源代码:

Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}

既然node对象起始的引用计数为1。为什么在打印时它的引用计数就为0了呢?node对象是在什么时候被释放的呢?

先别急。我们先让node不被释放,怎样使node不被释放,有两种方法:

方法1:通过retain()方法添加node引用计数

//添加node的引用计数
node->retain();

node最開始创建后引用计数为1,调用retain()方法使其引用计数再+1,此时node的引用计数为2。但刚刚也看到了,引用计数開始后不知什么时候减了1,所以打印出来应该是1。执行一下:

COCOS学习笔记--Cocod2dx内存管理(三)-Coco2d-x内存执行原理

能够看出,通过调用node的retain()方法能够使其引用计数+1。

方法2:通过addChild()方法将node加入到父节点上

this->addChild(node);

执行效果例如以下:

COCOS学习笔记--Cocod2dx内存管理(三)-Coco2d-x内存执行原理

能够看到。通过addChild()方法也能够使node的引用计数+1,这是为什么呢

我们再看一下Node类的源代码:

void Node::addChild(Node *child)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
this->addChild(child, child->_localZOrder, child->_name);
}

能够看到node的addChild()方法调用了其重载的addChild()方法,那我们就接着看这重载的addChild()方法都做了什么:

void Node::addChild(Node *child, int localZOrder, int tag)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
CCASSERT( child->_parent == nullptr, "child already added. It can't be added again"); addChildHelper(child, localZOrder, tag, "", true);
}

在重载addChild()方法中我们看到调用了这句:

addChildHelper(child, localZOrder, tag, "", true);

这句是干什么的呢我们接着看:

void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
...... this->insertChild(child, localZOrder); ......
}

在Node::addChildHelper()方法中调用了这句:

this->insertChild(child, localZOrder)
那我们就继续看insertChild()方法:
void Node::insertChild(Node* child, int z)
{
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
child->_localZOrder = z;
}

我们看到了运行了这句代码:

_children.pushBack(child);

这句非常关键。我们到CCvector.h文件里看它的详细实现:

  void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}

能够看到。通过pushBack方法将传来的child对象加入到了_data这个数据结构中,然后对child运行了其retain()方法,这下大家都明确了吧!为什么调用addChild()方法会使child的引用计数+1,由于它最后还是调用了retain()方法。

好了。以上两种添加引用计数的方法介绍完了。回过头来,刚刚另一个问题一直没有解决:就是我们创建的node明明在创建后引用计数为1,假设不人为通过以上2种方法添加其引用计数,为什么程序一启动引用计数就变成0了呢?这个node是在什么时候被释放的呢?

接下来我就为大家解答一下:

之前在我写渲染流程的博客(http://blog.csdn.net/gzy252050968/article/details/50414407)中提到过,引擎的入口函数是CCApplication类的run()方法。

 int Application::run()
{
......
director->mainLoop();//进入引擎的主循环
......
return 0;
}

在run()方法中进入了游戏的主循环mainLoop()。我们设置的帧率就是mainLoop()方法每秒运行的次数。一般默认是每秒运行60次。我们游戏的渲染、内存管理等等全都是在这个mainLoop()方法里不断运行的。

我们再看一下这个主循环mainLoop():

void DisplayLinkDirector::mainLoop()
{
if (_purgeDirectorInNextLoop)//进入下一个主循环,也就是结束这次的主循环。就净化,也就是一些后期处理
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
drawScene();//绘制屏幕
PoolManager::getInstance()->getCurrentPool()->clear();//释放一些没实用的对象。主要保件内存的合理管理
}
}

我们能够看到这句代码:

PoolManager::getInstance()->getCurrentPool()->clear();

这句就是在每一帧结束时释放没实用到的对象,详细过程是先通过PoolManager::getInstance()方法获得PoolManager的单例对象,然后再通过getCurrentPool()方法得到当前的自己主动释放池对象,最后运行AutoreleasePool的clear()方法。clear()方法我在上一篇博客里写过,这里再去CCAutoreleasePool.cpp中看一下:

void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;//设置为运行了清空操作
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
//遍历自己主动释放池managedObjectArray里存放的全部的Ref
for (const auto &obj : releasings)
{
//调用obj的release(),对obj的引用计数-1(假设对象引用计数为0则删除)
obj->release();
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;//设置为未运行清空操作
#endif
}

clear()方法就是AutoreleasePool对象把自己维护的队列managedObjectArray里面每个obj都运行release()。

release()方法我的上上篇博客里也介绍过,这里再看一下加深记忆:

void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
//对其引用计数值进行-1
--_referenceCount;
//引用计数为0,删除对象
if (_referenceCount == 0)
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
//从保存Ref*的list中删除
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
//删除该对象
delete this;
}
}

看到这句了没?

--_referenceCount;

看到这句了没?

delete this;

release()方法分两部分,先对引用计数-1,然后推断引用计数是否为0。若为0则删除对象。

假设我们仅仅通过create()去创建一个对象,而不去调用其retain()方法,那么这个对象就会在下一帧被释放掉。

这回你理解为什么我们一開始创建的node直接就被释放掉了吧。

好了,重点来了。我们看到人为通过以上2种方法让引用计数+1后,打印出来的引用计数一直是1,按理来说,它们是通过create()创建出来的。在每一帧结束后AutoreleasePool::clear()方法中也会调用其release()方法。这一帧是1。下一帧就该减1变成 0了啊,为什么没有变成0被释放掉呢?

为什么啊?为什么啊?

事实上是这种,我们每次运行AutoreleasePool::clear()后,都会对AutoreleasePool维护的队列_managedObjectArray运行一次clear(),也就是说在下一帧的时候。自己主动释放池里已经不存在这些node了,所以在AutoreleasePool::clear()中便不会运行之前这些node的release()方法。这些node在下一帧并不会被释放,这就是为什么node的引用计数打印出来一直是1,说白了就是在AutoreleasePool::clear()中每一个node仅仅会运行一次release()方法。

那么接下来你可能会想,既然在自己主动释放池中仅仅运行一次node的release()方法,那么怎样去删除node呢?

非常easy:之前介绍了2种添加node引用计数的方法,1是retain()方法2是addChild()方法,那么要删除node的方法与以上2个一一相应:

方法一.通过release()方法降低node引用计数;

方法二.通过removeFromParent()方法将node从父节点上移除。

removeFromParent()方法事实上也是在其方法中运行release()方法进行了删除操作。但该方法不是仅仅运行了单纯的删除操作,它还从渲染树中将node移除。

Cocos2d-x是通过渲染树进行渲染的。对cocos引擎渲染流程不是非常了解的能够看看我之前的博客,cocos是将全部节点加入到渲染树上进行渲染的。

最后总结一下:

1.create出的node对象起始引用计数为1;

2.添加node的引用计数方法有2种:调用retain()方法使引用计数直接+1或通过addChild()方法将node加入到父节点上使其引用计数+1;

3.降低node的引用计数方法有2种:调用release()方法使引用计数直接-1或通过removeFromParent()方法将node从父节点上移除。

4.主循环mainLoop()方法中在每帧都会运行AutoreleasePool::->clear()释放自己主动释放池中没实用到的对象;

5.假设仅仅通过create()去创建一个对象node。而不去调用其retain()方法。那么这个node就会在下一帧被释放掉。

好了,关于Cocos引擎内存管理的全部内容就都OK了。花了一个周末连学习带总结,累死宝宝了<@_@>