当前位置: 首页 > 工具软件 > Cocos2D-Swift > 使用案例 >

cocos2d-x架构一瞥——cocos2d-x的内存管理机制

梁和颂
2023-12-01

本文是阅读《我所理解的cocos2d-x》第二章——Cocos2d-x架构一瞥的2.1、2.2节两个小章节后的总结
(参考书籍《我所理解的cocos2d-x》,秦春林著)
(cocos2d-x版本为3.17)

在C++中,动态内存分配是一把双刃剑;一方面,直接访问内存地址提高了应用程序的性能及内存使用的灵活性;另一方面,由于程序没有正确的分配和释放而造成的野指针、重复释放、内存泄漏等问题又严重影响着应用程序的稳定性。

C++中使用new关键字在运行时给一个对象动态地分配内存,并返回堆上内存的地址供应用程序访问,被动态分配的内存需要在对象不再被使用时通过delete运算符将内存释放,归还给内存池。

如果不能够正确处理堆内存的分配与释放会导致以下问题。

1、野指针:指针指向的内存已经被释放

2、重复释放:重复释放已经释放的内存单元,或者释放一个野指针(也是重复释放)都会导致C++运行时错误。

3、内存泄漏:分配使用的内存单元没有被释放,就会被一直占用。

根据分配内存的方法,C++中有3种管理内存的方式

1、自动存储:函数内部的常规变量,存储在栈上,自动分配,自动释放内存。

2、静态存储:用于存储一些静态变量

3、动态存储:通过new关键字动态分配的内存单元

C++11使用3种不同的智能指针管理内存

1、unique_ptr:指针不能与其他智能指针共享所指对象的内存。

2、shared_ptr:多个shared_ptr指针可以共享同一分配的内存,它在实现上采用了引用计数。

3、weak_ptr:用来指向shared_ptr指针分配的对象内存,但不拥有该内存。(即指向的时候不会增加shared_ptr的引用计数),通常可用于验证shared_ptr指针的有效性。

cocos2dx为什么不使用上述3种C++智能指针?

1、智能指针有比较大的性能损失,shared_ptr为了保证线程安全,使用互斥锁保证所有线程访问时引用计数保持正确

2、虽然智能指针看起来很好用,但是使用的时候依旧需要显示声明。

cocos2dx的内存管理机制

1、引用计数(有点像是shared_ptr)

cocos2dx中的几乎所有对象都继承自Ref类。Ref类的主要指责也就是对对象的引用计数进行管理

class CC_DLL Ref
{
public:
    void retain();//引用计数+1
    void release();//引用计数-1
    Ref* autorelease();//将对象交给内存池
    unsigned int getReferenceCount() const;//获取当前的引用计数
protected:
    Ref();
    
public:
    virtual ~Ref();

protected:
    /// count of references
    unsigned int _referenceCount;//引用计数

    friend class AutoreleasePool;

#if CC_ENABLE_SCRIPT_BINDING
public:
    /// object id, ScriptSupport need public _ID
    unsigned int        _ID;
    /// Lua reference id
    int                 _luaID;
    /// scriptObject, support for swift
    void* _scriptObject;

    /**
     When true, it means that the object was already rooted.
     */
    bool _rooted;
#endif

#if CC_REF_LEAK_DETECTION
public:
    static void printLeaks();
#endif
};

首先我们看到数据成员 unsigned int _referenceCount;就是我们的引用计数。

看到Ref类的构造器

//可以看到,构造器初始化链表中,引用计数直接被赋值为1
Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
#if CC_ENABLE_SCRIPT_BINDING
, _luaID (0)
, _scriptObject(nullptr)
, _rooted(false)
#endif
{
#if CC_ENABLE_SCRIPT_BINDING
    static unsigned int uObjectCount = 0;
    _ID = ++uObjectCount;
#endif
    
#if CC_REF_LEAK_DETECTION
    trackRef(this);
#endif
}

转到retain函数的定义

//首先断言判断引用计数大于0,因为等于0的应该被释放掉内存
//断言判断之后就是引用计数+1
void Ref::retain()
{
    CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
    ++_referenceCount;
}

转到release函数的定义

//首先看到断言判断引用计数大于0
//引用计数-1
//减1之后判断引用计数是否为0,如果为0
//判断内存池是否正在清理以及对象是否在内存池中(如果在内存池中则报错,因为autoreleasePool的对象会遍历操作对象的引用计数
//判断之后,就可以将对象delete了,释放内存。
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))
        {
            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;
    }
}

我们看一下在工程开发过程中引用计数是如何变化的

auto node = new Node();//创建一个node对象,引用计数为1
addChild(node);//将node添加进当前对象中,引用计数为2
node->removeFromParent();//对象将自己从父节点中删除,此时的引用计数为1
node->release();//node对象将自己的引用计数再减一,变为零,内存被释放

我们看一下函数addChild和函数removeFromParent如何增加减少引用计数的

先来看addchild函数

//node类的addchild方法
void Node::addChild(Node* child, int localZOrder, const std::string &name)
{
    CCASSERT(child != nullptr, "Argument must be non-nil");
    CCASSERT(child->_parent == nullptr, "child already added. It can't be added again");
    
    addChildHelper(child, localZOrder, INVALID_TAG, name, false);//看此处,我们跳进去
}
//——————————————————————————————————————————————————
//node类的addChildHelper
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
    auto assertNotSelfChild
        ( [ this, child ]() -> bool
          {
              for ( Node* parent( getParent() ); parent != nullptr;
                    parent = parent->getParent() )
                  if ( parent == child )
                      return false;
              
              return true;
          } );
    (void)assertNotSelfChild;
    
    CCASSERT( assertNotSelfChild(),
              "A node cannot be the child of his own children" );
    
    if (_children.empty())
    {
        this->childrenAlloc();
    }
    
    this->insertChild(child, localZOrder);//我们看到这里,跳进去
    
    if (setTag)
        child->setTag(tag);
    else
        child->setName(name);
    
    child->setParent(this);

    child->updateOrderOfArrival();

    if( _running )
    {
        child->onEnter();
        // prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter
        if (_isTransitionFinished)
        {
            child->onEnterTransitionDidFinish();
        }
    }
    
    if (_cascadeColorEnabled)
    {
        updateCascadeColor();
    }
    
    if (_cascadeOpacityEnabled)
    {
        updateCascadeOpacity();
    }
}
//_________________________
//node类的insertChild
void Node::insertChild(Node* child, int z)
{
#if CC_ENABLE_GC_FOR_NATIVE_OBJECTS
    auto sEngine = ScriptEngineManager::getInstance()->getScriptEngine();
    if (sEngine)
    {
        sEngine->retainScriptObject(this, child);
    }
#endif // CC_ENABLE_GC_FOR_NATIVE_OBJECTS
    _transformUpdated = true;
    _reorderChildDirty = true;
    _children.pushBack(child);//我们看到这里!!!
    child->_setLocalZOrder(z);
}
//Vector的push方法,因为_children是一个Vector
    void pushBack(T object)
    {
        CCASSERT(object != nullptr, "The object should not be nullptr");
        _data.push_back( object );
        object->retain();//此处,引用计数+1
    }

再来看removeFromParent函数

void Node::removeFromParent()
{
    this->removeFromParentAndCleanup(true);
}

void Node::removeFromParentAndCleanup(bool cleanup)
{
    if (_parent != nullptr)
    {
        _parent->removeChild(this,cleanup);//我们跳进这里
    } 
}
//——————————————————————————————————————————————————
void Node::removeChild(Node* child, bool cleanup /* = true */)
{
    // explicit nil handling
    if (_children.empty())
    {
        return;
    }

    ssize_t index = _children.getIndex(child);
    if( index != CC_INVALID_INDEX )
        this->detachChild( child, index, cleanup );//我们跳进这个函数
}
//———————————————————————————————————————————————————————
void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
    // IMPORTANT:
    //  -1st do onExit
    //  -2nd cleanup
    if (_running)
    {
        child->onExitTransitionDidStart();
        child->onExit();
    }

    // If you don't do cleanup, the child's actions will not get removed and the
    // its scheduledSelectors_ dict will not get released!
    if (doCleanup)
    {
        child->cleanup();
    }
    
#if CC_ENABLE_GC_FOR_NATIVE_OBJECTS
    auto sEngine = ScriptEngineManager::getInstance()->getScriptEngine();
    if (sEngine)
    {
        sEngine->releaseScriptObject(this, child);
    }
#endif // CC_ENABLE_GC_FOR_NATIVE_OBJECTS
    // set parent nil at the end
    child->setParent(nullptr);

    _children.erase(childIndex);//我们再跳进这个函数
}
//————————————————————————————————————————————————————————
    iterator erase(ssize_t index)
    {
        CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
        auto it = std::next( begin(), index );
        (*it)->release();//在这个地方,引用计数减一
        return _data.erase(it);
    }
2、我们如何通过autorelease()方法声明一个“智能指针”

刚刚我们有看到,引用计数虽然帮助我们释放了内存,但是需要自己release,好麻烦

auto node = new Node();//创建一个node对象,引用计数为1
addChild(node);//将node添加进当前对象中,引用计数为2
node->removeFromParent();//对象将自己从父节点中删除,此时的引用计数为1
node->release();//node对象将自己的引用计数再减一,变为零,内存被释放

此时我们通过autorelease()来管理,cocos2dx使用autorelease()方法声明一个对象指针是智能指针,但是这些智能指针并不是单独关联某个自动变量,而是全部加进一个AutoReleasePool中,在每一帧结束的时候对autoReleasePool中的对象进行引用计数减1的处理。

我们看到Ref类的autorelease()方法

Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);//将当前对象加入当前的autoreleasePool()
    return this;
}
//————————————————————————————————
void AutoreleasePool::addObject(Ref* object)
{
    _managedObjectArray.push_back(object);//加进一个vector中
}
//在我们cocos2dx的主循环中
int Application::run()//太长了,删了很多代码=。=
{

    while(!glview->windowShouldClose())
    {
        QueryPerformanceCounter(&nNow);
        interval = nNow.QuadPart - nLast.QuadPart;
        if (interval >= _animationInterval.QuadPart)
        {
            nLast.QuadPart = nNow.QuadPart;
            director->mainLoop();//我们跳进这里
            glview->pollEvents();
        }
        else
        {
            waitMS = (_animationInterval.QuadPart - interval) * 1000LL / freq.QuadPart - 1L;
            if (waitMS > 1L)
                Sleep(waitMS);
        }
    }

    return 0;
}
//——————————————————————————————————————————
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的clear()方法
    }
}
//————————————————————————————————
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
    _isClearing = true;
#endif
    std::vector<Ref*> releasings;
    releasings.swap(_managedObjectArray);//此处使用vector的swap方法,将_managedObjectArray的对象全部移到releasings中,_managedObjectArray为空,不保留对象
    for (const auto &obj : releasings)
    {
        obj->release();//然后对releasings中的对象都release(),引用计数减一,这就是刚刚所说的,一帧过后所有的内存池的对象引用计数减一,但是只会减去一次。
    }
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
    _isClearing = false;
#endif
}
3、AutoreleasePool队列

默认AutoreleasePool一帧被清理一次,主要是用来清理UI元素的,因为UI元素大部分是添加到UI树上,会一直占用内存。

那些不被添加到UI树上,又没有retain()的UI元素呢?一定要等到一帧之后才能被清理掉。假设我们需要循环调用一个函数,这个函数里面会创建10个UI元素,循环100次,不添加进UI树,只是临时使用。那么在被清理前就会占用极大内存。

对于这种有特殊要求的自定义的数据对象,我们要能够自定义AutoreleasePool的生命周期!

//因为对象在调用autorelease()方法的时候是自动添加进最新的AutorekeasPool中
Ref* Ref::autorelease()
{
    PoolManager::getInstance()->getCurrentPool()->addObject(this);
    return this;
}
//————————————————————————————————————
AutoreleasePool* PoolManager::getCurrentPool() const
{
    return _releasePoolStack.back();//返回_releasePoolStack栈的最后(新)一个AutoreleasePool
}
//——————————————————————————————————————
std::vector<AutoreleasePool*> _releasePoolStack;//_releasePoolStack是一个vector,专门用来保存AutoreleasePool

PoolManager类的初始状态默认至少有一个AutoreleasePool,它主要用来存储前面讲述的cocos2d-x中的UI元素对象。

对于刚刚说的那种情况(创建大量的临时元素对象),我们可以创建自己的AutoreleasePool对象并加入_releasePoolStack的尾部。

AutoreleasePool在构造函数中将自身的指针添加进PoolManager的_releasePoolStack中。

AutoreleasePool::AutoreleasePool()
: _name("")
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
    _managedObjectArray.reserve(150);
    PoolManager::getInstance()->push(this);//此处将自身加进vector中
}
AutoreleasePool::~AutoreleasePool()
{
    CCLOGINFO("deallocing AutoreleasePool: %p", this);
    clear();//释放所有对象
    
    PoolManager::getInstance()->pop();//此处将自己弹出来
}

前面讲到Ref::autorelease()方法始终将自己添加到当前的AutoreleasePool中,那么我们申明一个AutoreleasePool对象就可以影响之后的所有对象,直至这个AutoreleasePool生命周期结束。

class Myclass:public Ref
{
public:
	static Myclass* create()
    {
    	auto ref = new Myclass;
    	return ref->autorelease;//将自己加进当前的AutoreleasePool中
    }
};
void customAutoreleasePool()
{
    AutoreleasePool pool;//创建AutoreleasePool对象
    auto ref1 = Myclass::create();//
    auto ref2 = Myclass::create();//都将自己加进新创建的pool中了
}
//当customAutoreleasePool函数结束,pool声明周期结束。其析构函数会先调用clear()方法释放对象,再把自己弹出vector
//这就通过AutoreleasePool管理我们的对象

cocos中的智能指针

cocos2dx-3.x引入了智能指针RefPtr < T >。RefPtr< T >是基于RAII实现的。RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

1、构造函数

RefPtr< T >需要依赖Ref的引用计数来管理内存,所有的类行T必须是Ref类型。Cocos2d-x通过静态转换static_const在编译时进行类型检查。

template <typename T> class RefPtr
{
public:
    RefPtr(RefPtr<T> && other)
    {
        _ptr = other._ptr;
        other._ptr = nullptr;//对于右值不会增加引用计数
    }

    RefPtr(T * ptr)
        : _ptr(ptr)
    {
        CC_REF_PTR_SAFE_RETAIN(_ptr);
    }
}

//————————————————————————————
#define CC_REF_PTR_SAFE_RETAIN(ptr)\
    \
    do\
    {\
        if (ptr)\
        {\
            const_cast<Ref*>(static_cast<const Ref*>(ptr))->retain();\//增加引用计数
        }\
    \
    }   while (0);
//————————————————————可以看到RefPtr变量和Ref指针是一种强引用关系,构造函数会对任何不是nullptr的Ref指针增加其引用计数,除非他是一个右值
2、赋值操作符

RefPtr< T >定义了一个对T的赋值操作符重载,。这样做可以使在对T进行转换的时候不用直接调用转换方法,从而对旧的资源进行释放。此外也可以使用nullptr让RefPtr成为一个空的智能指针。

与构造函数相似,对于任何左值变量的赋值,RefPtr都应该与该左值共享资源从而增加其引用计数,而对于右值,仍然保持转移而不是共享。与构造函数不同的是,赋值操作符除了会增加其资源的引用计数,还会释放对之前旧的资源的引用计数。

    RefPtr<T> & operator = (T * other)
    {
        if (other != _ptr)
        {
            CC_REF_PTR_SAFE_RETAIN(other);//增加引用计数
            CC_REF_PTR_SAFE_RELEASE(_ptr);//释放旧资源的引用计数
            _ptr = other;//赋值新的资源引用计数
        }
        
        return *this;
    }
#define CC_REF_PTR_SAFE_RETAIN(ptr)\
    \
    do\
    {\
        if (ptr)\
        {\
            const_cast<Ref*>(static_cast<const Ref*>(ptr))->retain();\
        }\
    \
    }   while (0);

#define CC_REF_PTR_SAFE_RELEASE(ptr)\
    \
    do\
    {\
        if (ptr)\
        {\
            const_cast<Ref*>(static_cast<const Ref*>(ptr))->release();\
        }\
    \
    }   while (0);
3、弱引用

赋值新的资源,但是不会增加引用计数

    void weakAssign(const RefPtr<T> & other)
    {
        CC_REF_PTR_SAFE_RELEASE(_ptr);//释放之前的资源
        _ptr = other._ptr;//赋值新的资源,但是不会增加引用计数
    }
4、RefPtr< T >与容器

Vector< T >的pushBack方法接收T *指针,RefPtr会调用自己重载的operator T *方法,加入Vector中。

加入Vector中的元素内存也由Vector进行共享管理

//RefPtr的方法   
operator T * () const { return _ptr; }
//cocos2dx 重写了Vector类,pushBack和popBack会分别retain和release
    void pushBack(T object)
    {
        CCASSERT(object != nullptr, "The object should not be nullptr");
        _data.push_back( object );
        object->retain();
    }
    
    /** Push all elements of an existing Vector to the end of current Vector. */
    void pushBack(const Vector<T>& other)
    {
        for(const auto &obj : other) {
            _data.push_back(obj);
            obj->retain();
        }
    }

    void popBack()
    {
        CCASSERT(!_data.empty(), "no objects added");
        auto last = _data.back();
        _data.pop_back();
        last->release();
    }
    
5、RefPtr< T >的缺陷

Cocos2d-x中的智能指针存在一些缺陷

1、引用计数可以被RefPtr从外部控制
//1、引用计数可以被RefPtr从外部控制
auto str = new _String("Hello");
RefPtr<_String>ptr;
ptr.weakAssign(str);//弱引用不增加引用计数,
str.release();//引用计数减1,变为0
(*ptr)->getCString();//操作野指针
2、RefPtr提供的弱引用表现为一个强引用的行为,它仍然可以对其资源进行修改
RefPtr<_String>ptr1(new _String("Hello"));//引用计数为2
RefPtr<_String>ptr2;
ptr2.weakAssign(ptr1);//弱引用不增加引用计数,依然为2
ptr2.reset();//引用计数减1
ptr2.reset();//被释放
(*ptr2)->getCString();//错误,操作野指针。

在C++中,弱引用的std::weak_ptr被限制只能通过其lock成员来访问原std::shared_ptr变量,从而对资源内存进行操作,这样做能保证智能指针的有效性。

总结:

怎么样进行内存管理

1、所有的UI元素尽量交给autorelease来管理,游戏中的数据则可以用智能指针RefPtr来管理。

2、Ref的引用计数并不是线程安全的。多线程中,我们需要互斥锁保证安全。

3、对自定义的Node的子类,添加create方法,并使该方法返回一个autorelease对象

4、对自定义的数据类型,如果需要动态分配内存就继承Ref,然后使用智能指针RefPtr管理内存。

5、不要动态分配AutoreleasePool对象,要始终使用自动变量。

6、不要显示调用RefPtr的构造函数,始终使用隐式方式调用构造函数,因为显示的构造函数会导致同时执行构造函数和赋值操作符,这会造成一次不必要的临时智能指针变量的产生。

 类似资料: