CocosUI的内存管理

寒假回家期间,在家下了一份cocos源码,在阅读的过程中也整理一下cocos的架构设计和实现,也算是为国隔离。
这次先讲一讲cocos对于UI元素的内存管理机制。之后本专栏还会写一些cocos其他模块的解析,例如渲染、事件分发等。
众所周知,C++是一种比较底层的语言,由于它目前还不支持垃圾回收机制,因此在堆上分配一个对象之后,必须在代码逻辑中由delete回收,否则就会导致内存泄漏问题。尤其对于游戏引擎来说,如果在每一帧的循环代码中出现内存泄漏,很容易内存就会爆掉。
C++11中可以使用shared_ptr来对指针进行引用计数,当引用为0时,自动释放指针指向的内存。但是shared_ptr为保证线程安全,运用互斥锁导致了一定性能损失,再加上使用shared_ptr的方式并不是很自然,因此Cocos没有用它来进行内存管理。

相反,Cocos设计了一种基于引用计数的方法来更简便和自然地保证创建的UI元素不会发生内存泄漏,在这里予以介绍。
在Cocos的UI中,所有的UI节点都是继承自Node类,Node又继承自Ref类,它负责UI节点的引用计数,类的声明如下:

class CC_DLL Ref
{
public:
    void retain();
    void release();
    Ref* autorelease();
    unsigned int getReferenceCount() const;

protected:
    Ref();

public:
    virtual ~Ref();

protected:
    /// count of references
    unsigned int _referenceCount;
    friend class AutoreleasePool;
};

其中_referenceCount中就记录了该对象的引用计数。retain和release分别负责增加和减少Ref对象的引用计数,代码如下:

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

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

    if (_referenceCount == 0)
        delete this;
}

调用release时,在减掉引用计数后,如果引用计数已经是0,则会清空掉自己所指向的内存。
当创建了一个Node节点时,此时它的引用计数为1,如果此时把它挂接在一个父节点上(比如addChild),则引用计数+1;如果将它从父节点remove掉,则引用计数-1。
另外说一句,cocos实现了一套自己的常用数据结构,挂接父节点时增加引用计数的操作是在cocos自己实现的CCVector里做的,我当时也找了很久:)

void pushBack(const Vector<T>& other)
{
    for(const auto &obj : other) {
        _data.push_back(obj);
        obj->retain();
    }
}

这样虽然做了一定的封装,但是还不够智能,需要程序逻辑手动负责release。因此cocos还在Ref类中设计了autorelease函数:

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

该函数会将对象自己交由全局对象PoolManager进行管理,PoolManager将它放入当前的对象池AutoreleasePool中。在每一帧结束的时候执行release操作,将节点的引用计数减1,并清除引用计数为1的节点:

void AutoreleasePool::clear()
{
    std::vector<Ref*> releasings;
    //swap函数会交换两个vector的空间,因此执行完这一句后_managedObjectArray长度为0
    releasings.swap(_managedObjectArray);
    for (const auto &obj : releasings)
        obj->release();
}

那么应该在什么时候来执行autorelease呢?相信你已经猜到了,cocos在执行创建节点的create函数时,自动调用autorelease,使其纳入PoolManager的管理中:

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

总结一下:Cocos通过Ref类,来存储节点的引用计数,并通过retain和release来增减引用计数的值。在create创建节点时,会自动调用autorelease函数来将节点添加到AutoreleasePool中。在创建节点的这一帧结束时,AutoreleasePool会对这一帧创建的所有节点调用release函数,并清空队列。这一帧结束后,若该节点已经挂接到父亲上,则引用计数为1,继续保留;若该节点没有父亲,则引用计数为0,被回收。因为已挂接的节点引用计数已经是1,因此再将它们从父亲detach掉时,会再次调用release,引用计数清0,从而被回收。
Cocos通过这种设计保证了在游戏运行时对内存的掌控。

原文地址:https://www.cnblogs.com/wickedpriest/p/12238338.html