通过上两篇博客。我们对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");
打印结果例如以下:
能够看到引用计数打印出来是一大串的整数,事实上这时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。执行一下:
能够看出,通过调用node的retain()方法能够使其引用计数+1。
方法2:通过addChild()方法将node加入到父节点上
this->addChild(node);
执行效果例如以下:
能够看到。通过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了。花了一个周末连学习带总结,累死宝宝了<@_@>