Cosos2dx中的内存管理机制

Why?

内存管理一直是个很重要的事情,需要考虑分配和回收的时机,千万不能出现内存泄漏,因为手机上内存本来就有限。内存管理的核心就是动态分配的内存及时回收。cocos2dx是从cocos2d-iphone发展过来的,使用的引用计数的机制进行管理。了解了cocos中的内存管理机制,就可以明确的了解在什么时候retian在什么时候release。我尽量只说关键的地方,不贴大量的代码。

引用计数

刚开始了解cocos的时候,看了一些书,书上说cocos中是使用的引用计数的机制进行管理内存。当时想了下,很好理解啊,每一帧去检查一下每个对象的引用计数,如果是0则释放掉。最近深入看了一下源码,发现不是每帧去检查每个对象,每个对象只会被检查一次,后面就在恰当的时机进行释放。

自动释放池

AutoreleasePool里面就是封装了一个vector,并对vector中的对象进行管理。

自动释放池管理

PoolManager,此类是对自动释放池的管理,看到有这个管理类,就知道自动释放池可能不止一个,是的,自动释放池可以创建多个,都由PoolManager这个类管理。PoolManager使用栈的操作来管理自动释放池。

Ref类

此类中有个变量保存引用计数,继承此类,就可以很方便的加入自动释放池中管理。引用计数初始的值为1

Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1

一个Node被管理的过程

1:创建一个node

Node* node = Node::create();

此时的node的引用计数是1
调用了Node的create工厂方法进行创建,查看create方法的实现

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

node调用了autorelease方法,此方法为Ref类的方法

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

其实是把当前的node放到了自动释放池中进行管理。PoolManager调用getInstance的方法获取实例,查看里面的实现

PoolManager* PoolManager::getInstance()
{
    if (s_singleInstance == nullptr)
    {
        s_singleInstance = new (std::nothrow) PoolManager();
        // Add the first auto release pool
        new AutoreleasePool("cocos2d autorelease pool");
    }
    return s_singleInstance;
}

可以看到自动释放池也是在此时创建的。以前引擎版本的自动释放池是在引擎初始化的时候创建的。
PoolManager类的getCurrentPool方法返回的是当前的自动释放池

AutoreleasePool* PoolManager::getCurrentPool() const
{
    return _releasePoolStack.back();
}

_releasePoolStack是一个vector,但是现在当成栈来用。

2:在mainLoop中进行处理

mainLoop是上篇文章说的主循环的操作,每一帧都会调用

void Director::mainLoop()
{
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();
    }
    else if (_restartDirectorInNextLoop)
    {
        _restartDirectorInNextLoop = false;
        restartDirector();
    }
    else if (! _invalid)
    {
        drawScene();
     
        // release the objects
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

此处调用了PoolManager::getInstance()->getCurrentPool()->clear()查看clear中的代码

std::vector releasings;
    releasings.swap(_managedObjectArray);
    for (const auto &obj : releasings)
    {
        obj->release();
    }

_managedObjectArray为自动释放池中的对象容器;releasings是个空的容器。
经过releasings.swap(_managedObjectArray)这个操作之后_managedObjectArray就是空的了,就是说加入了自动释放池中的对象只会被管理一次,后面不会再被管理了。
对releasings中的对象(其实就是刚才内存释放池中的对象)进行遍历,进行release操作,此为Ref类中的方法

void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
    --_referenceCount;

    if (_referenceCount == 0)
    {
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
        auto poolManager = PoolManager::getInstance();
        if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
        {
            // Trigger an assert if the reference count is 0 but the Ref is still in autorelease pool.
            // This happens when 'autorelease/release' were not used in pairs with 'new/retain'.
            //
            // Wrong usage (1):
            //
            // auto obj = Node::create();   // Ref = 1, but it's an autorelease Ref which means it was in the autorelease pool.
            // obj->autorelease();   // Wrong: If you wish to invoke autorelease several times, you should retain `obj` first.
            //
            // Wrong usage (2):
            //
            // auto obj = Node::create();
            // obj->release();   // Wrong: obj is an autorelease Ref, it will be released when clearing current pool.
            //
            // Correct usage (1):
            //
            // auto obj = Node::create();
            //                     |-   new Node();     // `new` is the pair of the `autorelease` of next line
            //                     |-   autorelease();  // The pair of `new Node`.
            //
            // obj->retain();
            // obj->autorelease();  // This `autorelease` is the pair of `retain` of previous line.
            //
            // Correct usage (2):
            //
            // auto obj = Node::create();
            // obj->retain();
            // obj->release();   // This `release` is the pair of `retain` of previous line.
            CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
        }
#endif

#if CC_ENABLE_SCRIPT_BINDING
        ScriptEngineProtocol* pEngine = ScriptEngineManager::getInstance()->getScriptEngine();
        if (pEngine != nullptr && pEngine->getScriptType() == kScriptTypeJavascript)
        {
            pEngine->removeObjectProxy(this);
        }
#endif // CC_ENABLE_SCRIPT_BINDING

#if CC_REF_LEAK_DETECTION
        untrackRef(this);
#endif
        delete this;
    }
}

大家可以看到,把引用计数减1,如果此时引用计数为0,则释放此Ref对象。
刚才创建的node,此时就会被释放掉了。如果使用了成员变量或者全局变量指向了刚才的node,那么刚才的node就是个野指针了。

3:添加到场景中的Node的引用计数情况

创建了的node添加到场景(其实也是个Node)中,此时引用计数就会+1,变成2

Node* node = Node::create();        // 此时node的引用计数为1
Layer* layer = Layer::create();
layer->addChild(node);                // 此时node的引用计数为2

为什么addChild了以后引用计数就变成2了呢,来看看代码

void Node::addChild(Node* child, int localZOrder, const std::string &name)
    调用addChildHelper(child, localZOrder, INVALID_TAG, name, false)
        调用Node::insertChild(Node* child, int z)
            调用_children.pushBack(child)
                调用_data.push_back( object );
                    object->retain();
                        调用++_referenceCount;

原来Node(Layer继承自Node)内部有个CCVector,每次添加Ref到CCVector都会retain一下,会增加一次引用计数。
相应的在removechild的时候会调用release,此时引用计数减一,如果此时引用计数为0了,则立刻释放,就不用在自动释放池中释放了。所以自动释放池只要管理一次就可以了。

4:如何让Ref不被释放

其实很简单,retain一下就可以了。
调用addChild方法会增加一次引用计数。有些Ref对象是不用添加到场景中的,比如CCArray,只是用来对场景中的元素进行管理,那么在创建一个CCArray对象以后,主动retain一下,就不会被自动释放池清除掉。但是在场景结束时,要relase掉,不然就会出现内存泄漏。

自动释放池的嵌套

如果创建了大量的Ref变量,那么在mainLoop的时候就有可能出现释放时间过长,导致下一次绘图间隔太久,导致界面上卡顿。如果有此种情况,可以考虑自己创建自动释放池来存放Ref变量,用完就释放掉。
此时PoolManager的管理功能就派上用场了。下面来看看这个过程。
使用场景:创建了大量的String(cocos中封装的字符类,非标准库中的类)对象对字符串进行操作,由于字符对象是不用加载到场景中的,所以在使用完成以后在本帧结束后就会被释放掉。

AutoreleasePool* pool = new AutoreleasePool("My release pool");        // 创建我们自己的auto release pool

String* string1 = String::createWithFormate("nihao%d", 1);
...
...
String* string1000 = String::createWithFormate("nihao%d", 1000);

delete pool;                // 释放我们自己创建auto release pool

分析一下上述流程:
1:创建了一个我们自己的自动释放池名字叫:My release pool。查看AutoreleasePool的构造函数

AutoreleasePool::AutoreleasePool(const std::string &name)
: _name(name)
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
    _managedObjectArray.reserve(150);
    PoolManager::getInstance()->push(this);
}

PoolManager会将我们刚刚创建的自动释放池加入到内部的栈中。此时通过getCurrentPool获取的就是刚才创建的自动释放池。

2:创建字符串变量指针,createWithFormate里面调用了autorelease函数,此时这些变量的指针都会添加到我们刚才创建的自动释放池中。

3:删除自动释放池。查看AutoreleasePool的析构函数。

AutoreleasePool::~AutoreleasePool()
{
    CCLOGINFO("deallocing AutoreleasePool: %p", this);
    clear();
    
    PoolManager::getInstance()->pop();
}

此时会先调用clear函数释放池中的指针。然后PoolManager会将当前的自动释放池从内部的栈中移除。此时PoolManager调用getCurrentPool返回的就是默认的自动释放池。

总结:

1:自动释放池对每个Ref对象指针管理一次,后面就在恰当的时机进行释放。
2:CCVetctor在添加Ref对象指针的时候会retain一次,在移除的时候会release一次。
3:自动释放池可以嵌套使用。

你可能感兴趣的:(Cosos2dx中的内存管理机制)