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


    通过上两篇博客。我们对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了。花了一个周末连学习带总结,累死宝宝了<@_@> 

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    2020牛客寒假算法基础集训营4-I 匹配星星【贪心】
    P1880 [NOI1995]石子合并【区间dp】
    P1280 尼克的任务
    P1041 传染病控制【暴搜】
    Heavy Transportation POJ
    【空间】C++内存管理
    【编译器】G++常用命令
    【NOIP2011】【Luogu1003】铺地毯
    【Luogu1739】表达式括号匹配
    【Luogu1160】队列安排
  • 原文地址:https://www.cnblogs.com/slgkaifa/p/6985424.html
Copyright © 2020-2023  润新知