欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Cocos2d-x的内存管理总结

程序员文章站 2022-06-04 12:18:06
cocos2d-x引擎的核心是用c++编写的,那对于所有使用该引擎的游戏开发人员来说,内存管理是一道绕不过去的坎。 关于cocos2d-x内存管理,网上已经有了许多参考资...

cocos2d-x引擎的核心是用c++编写的,那对于所有使用该引擎的游戏开发人员来说,内存管理是一道绕不过去的坎。

关于cocos2d-x内存管理,网上已经有了许多参考资料,有些资料写的颇为详实,因为在内存管理这块我不想多费笔墨,只是更多的将思路描述清楚。

一、对象内存引用计数

cocos2d-x内存管理的基本原理就是对象内存引用计数,cocos2d-x将内存引用计数的实现放在了顶层父类ccobject中,这里将涉及引用计数的ccobject的成员和方法摘录出来:

复制代码 代码如下:

class cc_dll ccobject : public cccopying
{
public:
   … …
protected:
    // count of references
    unsigned int        m_ureference;
    // count of autorelease
    unsigned int        m_uautoreleasecount;
public:
    void release(void);
    void retain(void);
    ccobject* autorelease(void);
    … ….
}

ccobject::ccobject(void)
: m_nluaid(0)
, m_ureference(1) // when the object is created, the reference count of it is 1
, m_uautoreleasecount(0)
{
  … …
}

void ccobject::release(void)
{
    ccassert(m_ureference > 0, "reference count should greater than 0");
    –m_ureference;

    if (m_ureference == 0)
    {
        delete this;
    }
}

void ccobject::retain(void)
{
    ccassert(m_ureference > 0, "reference count should greater than 0");

    ++m_ureference;
}

ccobject* ccobject::autorelease(void)
{
    ccpoolmanager::sharedpoolmanager()->addobject(this);
    return this;
}


先不考虑autorelease与m_uautoreleasecount(后续细说)。计数的核心字段是m_ureference,可以看到:

* 当一个object初始化(被new出来时),m_ureference = 1;
* 当调用该object的retain方法时,m_ureference++;
* 当调用该object的release方法时,m_ureference–,若m_ureference减后为0,则delete该object。

二、手工对象内存管理

在上述对象内存引用计数的原理下,我们得出以下cocos2d-x下手工对象内存管理的基本模式:

复制代码 代码如下:

ccobject *obj = new ccobject();
obj->init();
…. …
obj->release();

在cocos2d-x中ccdirector就是一个手工内存管理的典型:

ccdirector* ccdirector::shareddirector(void)
{
    if (!s_shareddirector)
    {
        s_shareddirector = new ccdisplaylinkdirector();
        s_shareddirector->init();
    }

    return s_shareddirector;
}

void ccdirector::purgedirector()
{
    … …
    // delete ccdirector
    release();
}

三、自动对象内存管理

所谓的“自动对象内存管理”,指的就是哪些不再需要的object将由cocos2d-x引擎替你释放掉,而无需你手工再调用release方法。

自动对象内存管理显然也要遵循内存引用计数规则,只有当object的计数变为0时,才会释放掉对象的内存。

自动对象内存管理的典型模式如下:

复制代码 代码如下:

ccyourclass *ccyourclass::create()
{
    ccyourclass*pret = new ccyourclass();
    if (pret && pret->init())
    {
        pret->autorelease();
        return pret;
    }
    else
    {
        cc_safe_delete(pret);
        return null;
    }
}

一般我们通过一个单例模式创建对象,与手工模式不同的地方在于init后多了一个autorelease调用。这里再把autorelease调用的实现摘录一遍:

复制代码 代码如下:

ccobject* ccobject::autorelease(void)
{
    ccpoolmanager::sharedpoolmanager()->addobject(this);
    return this;
}

追溯addobject方法:

复制代码 代码如下:

// cocoa/ccautoreleasepool.cpp

void ccpoolmanager::addobject(ccobject* pobject)
{
    getcurreleasepool()->addobject(pobject);
}

void ccautoreleasepool::addobject(ccobject* pobject)
{
    m_pmanagedobjectarray->addobject(pobject);

    ccassert(pobject->m_ureference > 1, "reference count should be greater than 1");
    ++(pobject->m_uautoreleasecount);
    pobject->release(); // no ref count, in this case autorelease pool added.
}

// cocoa/ccarray.cpp
void ccarray::addobject(ccobject* object)                                                                                                  
{                                                                                                                                         
    ccarrayappendobjectwithresize(data, object);                            

// support/data_support/cccarray.cpp
void ccarrayappendobjectwithresize(ccarray *arr, ccobject* object)                                                                         
{                                                                                                                  
    ccarrayensureextracapacity(arr, 1);                                                             
    ccarrayappendobject(arr, object);                                        
}

void ccarrayappendobject(ccarray *arr, ccobject* object)
{
    ccassert(object != null, "invalid parameter!");
    object->retain();
    arr->arr[arr->num] = object;
    arr->num++;
}

调用层次挺深,涉及的类也众多,这里归纳总结一下。

cocos2d-x的自动对象内存管理基于对象引用计数以及ccautoreleasepool(自动释放池)。引用计数前面已经说过了,这里单说自动释放池。cocos2d-x关于自动对象内存管理的基本类层次结构如下:

复制代码 代码如下:

    ccpoolmanager类 (自动释放池管理器)
        – ccarray*    m_preleasepoolstack; (自动释放池栈,存放ccautoreleasepool类实例)

    ccautoreleasepool类
        – ccarray*    m_pmanagedobjectarray; (受管对象数组)

ccobject关于内存计数以及自动管理有两个字段:m_ureference和m_uautoreleasecount。前面在手工管理模式下,我只提及了m_ureference,是m_uautoreleasecount该亮相的时候了。我们沿着自动释放对象的创建步骤来看看不同阶段,这两个重要字段的值都是啥,代表的是啥含义:
复制代码 代码如下:

ccyourclass*pret = new ccyourclass();    m_ureference = 1; m_uautoreleasecount = 0;
pret->init();                           m_ureference = 1; m_uautoreleasecount = 0;
pret->autorelease();                   
    m_pmanagedobjectarray->addobject(pobject); m_ureference = 2; m_uautoreleasecount = 0;
    ++(pobject->m_uautoreleasecount);          m_ureference = 2; m_uautoreleasecount = 1;
    pobject->release();                        m_ureference = 1; m_uautoreleasecount = 1;

在调用autorelease之前,两个值与手工模式并无差别,在autorelease后,m_ureference值没有变,但m_uautoreleasecount被加1。

m_uautoreleasecount这个字段的名字很容易让人误解,以为是个计数器,但实际上绝大多数时刻它是一个标识的角色,以前版本代码中有一个布尔字段m_bmanaged,似乎后来被m_uautoreleasecount替换掉了,因此m_uautoreleasecount兼有m_bmanaged的含义, 也就是说该object是否在自动释放池的控制之下,如果在自动释放池的控制下,自动释放池会定期调用该object的release方法,直到该 object内存计数降为0,被真正释放。否则该object不能被自动释放池自动释放内寸,需手工release。这个理解非常重要,再后面我们能用到这个理解。


四、自动释放时机

通过autorelease我们已经将object放入autoreleasepool中,那究竟何时对象会被释放呢?答案是每帧执行一次自动内存对象释放操作。

在“hello,cocos2d-x”一文中,我们讲过整个cocos2d-x引擎的驱动机制在于glthread的guardedrun函数,后者会 “死循环”式(实际帧绘制频率受到屏幕vertsym信号的影响)的调用render的ondrawframe方法实现,而最终程序会进入 ccdirector::mainloop方法中,也就是说mainloop的执行频率是每帧一次。我们再来看看mainloop的实现:

复制代码 代码如下:

void ccdisplaylinkdirector::mainloop(void)
{
    if (m_bpurgedirecotorinnextloop)
    {
        m_bpurgedirecotorinnextloop = false;
        purgedirector();
    }
    else if (! m_binvalid)
     {
         drawscene();

         // release the objects
         ccpoolmanager::sharedpoolmanager()->pop();
     }
}

这次我们要关注的不是drawscene,而是 ccpoolmanager::sharedpoolmanager()->pop(),显然在游戏未退出 (m_bpurgedirecotorinnextloop决定)的条件下,ccpoolmanager的pop方法每帧执行一次,这就是自动释放池执行的起点。

复制代码 代码如下:

void ccpoolmanager::pop()
{
    if (! m_pcurreleasepool)
    {
        return;
    }

     int ncount = m_preleasepoolstack->count();

    m_pcurreleasepool->clear();

      if(ncount > 1)
      {
        m_preleasepoolstack->removeobjectatindex(ncount-1);
        m_pcurreleasepool = (ccautoreleasepool*)m_preleasepoolstack->objectatindex(ncount – 2);
    }
}

真正释放对象的方法是m_pcurreleasepool->clear()。

复制代码 代码如下:

void ccautoreleasepool::clear()
{
    if(m_pmanagedobjectarray->count() > 0)
    {
        ccobject* pobj = null;
        ccarray_foreach_reverse(m_pmanagedobjectarray, pobj)
        {
            if(!pobj)
                break;

            –(pobj->m_uautoreleasecount);
        }

        m_pmanagedobjectarray->removeallobjects();
    }
}

void ccarray::removeallobjects()    
{  
    ccarrayremoveallobjects(data);                   
}

void ccarrayremoveallobjects(ccarray *arr)                   
{                      
    while( arr->num > 0 )                     
    {                   
        (arr->arr[--arr->num])->release();              
    }                   
}

不出预料,当前自动释放池遍历每个“受控制”object,–m_uautoreleasecount,并调用该object的release方法。

我们接着按释放流程来看看m_uautoreleasecount和m_ureference值的变化:

复制代码 代码如下:

ccpoolmanager::sharedpoolmanager()->pop();  m_ureference = 0; m_uautoreleasecount = 0;

五、自动释放池的初始化

自动释放池本身是何时出现的呢?回顾一下cocos2d-x引擎的初始化过程(android版),引擎初始化实在render的onsurfacecreated方法中进行的,我们不难追踪到以下代码:

复制代码 代码如下:

//hellocpp/jni/hellocpp/main.cpp
java_org_cocos2dx_lib_cocos2dxrenderer_nativeinit {

    //这里ccdirector第一次被创建
    if (!ccdirector::shareddirector()->getopenglview())
    {
        cceglview *view = cceglview::sharedopenglview();
        view->setframesize(w, h);

        appdelegate *pappdelegate = new appdelegate();
        ccapplication::sharedapplication()->run();
    }
}

ccdirector* ccdirector::shareddirector(void)
{
    if (!s_shareddirector)
    {
        s_shareddirector = new ccdisplaylinkdirector();
        s_shareddirector->init(); 
    }

    return s_shareddirector;
}

bool ccdirector::init(void)
{
    setdefaultvalues();

    … …

    // create autorelease pool
    ccpoolmanager::sharedpoolmanager()->push();

    return true;
}

六、探寻cocos2d-x内核对象的自动化内存释放

前面我们基本了解了cocos2d-x的自动化内存释放原理。如果你之前翻看过一些cocos2d-x的内核源码,你会发现很多内核对象都是通过单例模式create出来的,也就是说都使用了autorelease将自己放入自动化内存释放池中被管理。

比如我们在hellocpp中看到过这样的代码:

复制代码 代码如下:

//helloworldscene.cpp
bool helloworld::init() {
     …. ….
    // add "helloworld" splash screen"
    ccsprite* psprite = ccsprite::create("helloworld.png");

    // position the sprite on the center of the screen
    psprite->setposition(ccp(visiblesize.width/2 + origin.x, visiblesize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addchild(psprite, 0);
    … …
}

ccsprite采用自动化内存管理模式create object(cocos2dx/sprite_nodes/ccsprite.cpp),之后将自己加入到helloworld这个cclayer实例 中。按照上面的分析,create结束后,ccsprite object的m_ureference = 1; m_uautoreleasecount = 1。一旦如此,那么在下一帧时,该object就会被ccpoolmanager释放掉。但我们在屏幕上依旧可以看到该sprite的存在,这是怎么回事呢?

问题的关键就在this->addchild(psprite, 0)这行代码中。addchild方法实现在cclayer的父类ccnode中:

复制代码 代码如下:

//  cocos2dx/base_nodes/ccnode.cpp
void ccnode::addchild(ccnode *child, int zorder, int tag)
{
    … …
    if( ! m_pchildren )
    {
        this->childrenalloc();
    }

    this->insertchild(child, zorder);

    … …
}

void ccnode::insertchild(ccnode* child, int z)
{
    m_breorderchilddirty = true;
    ccarrayappendobjectwithresize(m_pchildren->data, child);
    child->_setzorder(z);
}

void ccarrayappendobjectwithresize(ccarray *arr, ccobject* object)
{
    ccarrayensureextracapacity(arr, 1);
    ccarrayappendobject(arr, object);
}

void ccarrayappendobject(ccarray *arr, ccobject* object)
{
    ccassert(object != null, "invalid parameter!");
    object->retain();
    arr->arr[arr->num] = object;
    arr->num++;
}

又是一系列方法调用,最终我们来到了ccarrayappendobject方法中,看到了陌生而又眼熟的retain方法调用。

在本文开始我们介绍ccobject时,我们知道retain是ccobject的一个方法,用于增加m_ureference计数。而实际上retain还隐含着“保留”这层意思。

在完成this->addchild(psprite, 0)调用后,csprite object的m_ureference = 2; m_uautoreleasecount = 1,这很关键。

我们在脑子里再过一下自动释放池释放object的过程:–m_ureference, –m_uautoreleasecount。一帧之后,两个值变成了m_ureference = 1; m_uautoreleasecount = 0。还记得前面说过的m_uautoreleasecount的另外一个非计数含义么,那就是表示该object是否“受控”,现在值为0,显然不再受自动释放池的控制了,后续即便再执行100次内存自动释放,也不会影响到该object的存活。

后续要想释放这个“精灵”,我们还是需要手工调用release,或再调用其autorelease方法。