Cocod2dx 节点生命周期/内存管理(三)-Coco2d-x内存运行原理

https://www.cnblogs.com/songcf/p/3162414.html#2798650
https://blog.csdn.net/yjhdxflqm/article/details/51088063
https://blog.csdn.net/gzy252050968/article/details/50471716 – 内存 博主吊吊的
https://blog.csdn.net/gzy252050968/article/details/50414407 – 渲染流程
CocosCreator客户端优化系列(三):内存优化

在这里插入图片描述
在这里插入图片描述
CocosCreator ScrollView性能优化–https://blog.csdn.net/zzx023/article/details/99851564


在Cocos2d-x中有这么几个函数,非常的常见。
virtual bool init( );
virtual void onEnter( );
virtual void onEnterTransitionDidFinish( );
virtual void onExitTransitionDidStart( );
virtual void onExit( );
virtual void cleanup( );
这6个函数都是Node节点类中的虚函数,也就是说,它的子类可以重写这些函数。而且这些函数的执行有一定的顺序。
情况一:单个场景从初始化 –> 退出 过程
bool init(){
if(!Layer::init()){ //父类的总是要先执行
return false;
}
return true;
}
void onEnter( ){
Layer::onEnter( );
}
void onEnterTransitionDidFinish( ){
Layer::onEnterTransitionDidFinish( );
}
注意:没有执行onExitTransitionDidStart(),因为只有一个场景。
void onExit( ){
Layer::onExit( );
}
void cleanup( ){
Layer::cleanup( );
}

情况二:场景A 跳转到 场景B 【replaceScene】
B: bool init( );
A : onExitTransitionDidStart( );
B : void onEnter( );
A : void onExit( );
B : void onEnterTransitionDidFinish( );
A : void cleanup( );
情况三: 场景A 跳转到 场景B 【pushScene】
注意少了 void cleanup( );函数
B: bool init( );
A : void onExitTransitionDidStart( );
B : void onEnter( );
A : void onExit( );
B : void onEnterTransitionDidFinish( );
情况四: 从场景B 跳转到 场景A 【popScene】
注意没有 A: bool init( ); 因为不用初始化2次。
B: void onExitTransitionDidStart( );
B: void onExit( );
B : void cleanup( );
A: void onEnter( );
A : void onEnterTransitionDidFinish( );

原文链接:https://blog.csdn.net/yjhdxflqm/article/details/51088063


通过上两篇博客,我们对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呢?

很简单:之前介绍了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了,花了一个周末连学习带总结,累死宝宝了<@_@>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值