Cocos2d-x 3.2 的内存管理详解


目标读者:了解 Cocos2d-x 中的节点以及节点树,了解引用计数,了解游戏主循环等概念。html


本文首先介绍 Cocos2d-x 3.2 中内存管理的做用,以及各个做用的应用。借由通俗易懂的解释来了解内存管理的过程。其次经过源码解析介绍其内部的实现原理。加深理解,从而在有须要的时候绕开引擎创建本身的内存管理机制。node

1、Cocos2d-x 3.2 内存管理的两个方面


1) 经过加入 autorelease 来自动释放那些建立后未使用的对象。
2) 经过节点管理来保证对象在弃用后及时地删除。git

一、及时释放弃用的对象

使用条件:该对象是Node的子类对象
使用方法:addChild、removeChildgithub

内存管理过程:数组

addChild //添加对象后,对象能够被使用
removeChild //删除对象后,对象被马上删除(经过 delete)

二、及时释放未使用的对象

简述新建立的对象若是一帧内不使用,就会被自动释放。(所谓一帧,便是一个gameloop。)
使用条件:对象经过CREAT_FUNC()宏建立或者对象使用autorelease加入了自动释放池。
使用方法:自动实现函数

内存管理过程:oop

  • 对象不使用的状况
对象建立 引用+1
对象自动释放 引用-1
  • 对象使用的状况
对象建立 引用+1
对象使用 引用+1 // 经过 addChild 使用对象
对象自动释放 引用-1

引用的初始值为0,若是一帧结束后对象的引用值仍是0,那就就会被 delete。性能



2、内存管理的实现原理

涉及内存管理的文件不少,仅展现直接相关的部分代码。this

一、第一部分

Ref 类: 进行引用计数、提供加入自动释放池的接口。lua

AutoreleasePool 类: 管理一个 vector 数组来存放加入自动释放池的对象。提供对释放池的清空操做。

PoolManager 类: 管理一个 vector 数组来存放自动释放池。默认状况下引擎只建立一个自动释放池,所以这个类是提供给开发者使用的,例如出于性能考虑添加本身的自动释放池。

DisplayLinkDirector 类: 这是一个导演类,提供游戏的主循环,实现每一帧的资源释放。这个类的名字看起来有点怪,可是不用管它。由于这个类继承了 Director 类,也是惟一一个继承了 Director 的类,也就是说彻底能够合并为一个类,引擎开发者在源码中有部分说明。

1.1 Ref

// 引用计数变量
unsigned int _referenceCount;

// 对象被构造后,引用计数值为 1
Ref::Ref()
: _referenceCount(1) //当Ref对象被建立时,引用计数的值为 1
{
#if CC_ENABLE_SCRIPT_BINDING
    static unsigned int uObjectCount = 0;
    _luaID = 0;
    _ID = ++uObjectCount;
    _scriptObject = nullptr;
#endif
    
#if CC_USE_MEM_LEAK_DETECTION
    trackRef(this);
#endif
}

// 引用+1
void Ref::retain()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    ++_referenceCount;
}

// 引用-1 。若是引用为0则释放对象
void Ref::release()
{
    CCASSERT(_referenceCount > 0, "reference count should greater than 0");
    --_referenceCount;

    if (_referenceCount == 0)
    {

#if CC_USE_MEM_LEAK_DETECTION
        untrackRef(this);
#endif
        delete this; // 注意这里 把对象 delete 了
    }
}

// 提供加入自动释放池的接口。对象调用此函数便可加入自动释放池的管理。
Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}

//获取引用计数值
unsigned int Ref::getReferenceCount() const
{
    return _referenceCount;
}

1.2 AutoreleasePool

// 存放释放池对象的数组
std::vector<Ref*> _managedObjectArray;

// 往释放池添加对象
void AutoreleasePool::addObject(Ref* object)
{
    _managedObjectArray.push_back(object);
}

// 清空释放池,将其中的全部对象都 delete
void AutoreleasePool::clear()
{
    // 释放全部对象
    for (const auto &obj : _managedObjectArray)
    {
        obj->release();
    }
    // 清空vector数组
    _managedObjectArray.clear();
}

// 查看某个对象是否在释放池中
bool AutoreleasePool::contains(Ref* object) const
{
    for (const auto& obj : _managedObjectArray)
    {
        if (obj == object)
            return true;
    }
    return false;
}

1.3 PoolManager

// 释放池管理器单例对象
static PoolManager* s_singleInstance;

// 释放池数组
std::vector<AutoreleasePool*> _releasePoolStack;

// 获取 释放池管理器的单例
PoolManager* PoolManager::getInstance()
{
    if (s_singleInstance == nullptr)
    {
        // 新建一个管理器对象
        s_singleInstance = new PoolManager(); 
        
        // 添加一个自动释放池
        new AutoreleasePool("cocos2d autorelease pool");// 内部使用了释放池管理器的push,这里的调用很微妙,读者能够动手看一看
    }
    return s_singleInstance;
}

// 获取当前的释放池
AutoreleasePool* PoolManager::getCurrentPool() const
{
    return _releasePoolStack.back();
}

// 查看对象是否在某个释放池内
bool PoolManager::isObjectInPools(Ref* obj) const
{
    for (const auto& pool : _releasePoolStack)
    {
        if (pool->contains(obj))
            return true;
    }
    return false;
}

// 添加释放池对象
void PoolManager::push(AutoreleasePool *pool)
{
    _releasePoolStack.push_back(pool);
}

// 释放池对象出栈
void PoolManager::pop()
{
    CC_ASSERT(!_releasePoolStack.empty());
    _releasePoolStack.pop_back();
}

1.4 DisplayLinkDirector

void DisplayLinkDirector::mainLoop()
{
    //第一次当导演
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();//进行清理工做
    }
    else if (! _invalid)
    {
        // 绘制场景,游戏主要工做都在这里完成
        drawScene();
     
        // 清空资源池
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

根据目前的分析,咱们先来捋一捋,待会儿再进一步深刻。内存管理的过程是怎么样的呢?首先,建立了一个 Node 对象A,Node 继承Ref,所以 Ref 的引用计数为1;而后,A经过 autorelease 将本身放入自动释放池;drawScene() 完成后,一帧结束,Director 经过释放池将池中的对象 clear(),即对 Node 对象A进行 release() 操做。A的引用计数变为0,执行 delete 释放A对象。

接下来咱们继续介绍另外几个与内存管理有关的类。

二、第二部分

Node 类:提供了 addChildremoveChild 方法来建立游戏的节点树。
Vector 类:封装了对于对象的 retain 操做和 release 操做。

2.1 Node

// 添加节点
void Node::addChild(Node *child)
{
    CCASSERT( child != nullptr, "Argument must be non-nil");
    this->addChild(child, child->_localZOrder, child->_name); // 通过这个方法-->addChildHelper-->insertChild,完成retain操做
}

// 移除节点
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
    //
    if (_children.empty())
    {
        return;
    }

    // 
    ssize_t index = _children.getIndex(child);
    if( index != CC_INVALID_INDEX )
        this->detachChild( child, index, cleanup );//注意这个函数
}

// 插入节点
void Node::insertChild(Node* child, int z)
{
    _transformUpdated = true;
    _reorderChildDirty = true;
    _children.pushBack(child);// pushBack方法对节点进行了retain
    child->_setLocalZOrder(z);
}


// 剥离节点
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
    
    ...// 部分省略

    _children.erase(childIndex);// erase方法对节点进行了release
}

2.2 Vector

// 这里仅展现与Node类相关的内存管理的部分

// 将对象入栈,引用+1
void pushBack(T object)
{
    CCASSERT(object != nullptr, "The object should not be nullptr");
    _data.push_back( object );

    object->retain(); // 进行了retain
}

// 将目标位置的对象移除
iterator erase(ssize_t index)
{
    CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
    auto it = std::next( begin(), index );

    (*it)->release(); // 进行了release

    return _data.erase(it);
}

到这里,终于能够把故事完整地讲一遍了。内存管理的过程是怎么样的呢?首先,建立了一个 Node 对象A, Node 继承 Ref,所以 Ref 的引用计数为1;而后A又经过 autorelease 将本身放入自动释放池;接着,有个 Node 对象B,B经过 addChild(A) 使得A的引用+1;几个 mainLoop 后,B经过 removeChild(A) 使得A的引用-1;这个 mainLoopdrawScene() 完成后,一帧结束, Director 经过释放池将池中的对象 clear(),即对 Node 对象A进行 release 操做。A的引用计数变为0,执行 delete 释放A对象。

三、高阶用法

之因此称为高阶用法,是由于,若是开发者对 Cocos 的内存管理机制理解不够深入,那么极可能会用错而致使损失大于收益。另外一方面,这类用法在平时不多会用到。

3.1 使用 retain 来延长对象的生存时间

在开发过程当中,若是须要使用一个节点对象,可是又不想把它放到节点树里面去。那么就可使用 retain 来避免对象被自动释放掉。

3.2 使用 PoolManagerpush 来延长对象的生存时间

有些状况下,但愿闲置对象晚一帧进行销毁,可使用 push 把当前释放池推入栈底,那么这一帧结束的时候只会释放刚 push 进去的释放池。

笔者自己尚未机会使用太高阶用法,若是有小伙伴发现了高阶用法在实际问题中的应用,敬请留言交流。

4、参考连接

深刻理解 Cocos2d-x 内存管理 (从这篇文章中得到许多启发。)
Cocos2d-x源码(位于Github上,若是连不上尝试使用前缀 https)
引用计数——维基百科(关于引用计数的说明)
Cocos2d-x的内存管理机制概述(里面提到了为何要有 PoolManager)
cocos2dx 3.2 (24)——内存管理机制(编纂地比较详细)

相关文章
相关标签/搜索