您的位置:首页 > 移动开发 > Cocos引擎

Cocos2d-x内存管理研究<二>(转载特兹卡特的百度空间)

2015-01-07 15:42 363 查看
  上一篇我们讲了内核是如何将指针加入管理类进行管理.这次我将分析一下内核是如何自动释放指针的.

  不过在这之前,我们要先引入另一个类.Cocos2d-x的核心类之一CCDirector.这个类可以说是引擎最主要的类了,没有他,引擎内的所有东西都无法运转起来.由于这个类有点大,做的事情很多,我就不一一粘出来看了,我们只关心跟内存管理有关的函数.
  因为一部片子只需要一个导演,所以CCDirector也就是一个单例类,是多重继承自CCObject和TypeInfo,有什么用,暂时不管.不过是采用的二段构建的单例.什么是二段构建,说简单点,就是不在构造函数中做初始化工作,而在另一个函数里做初始化工作.这样做有什么好处?可以说没有好处,也可以说有好处.的看用的人是出于一种什么样的目的了.比如说我们这里,如果不用二段构建是不可能实现的.因为CCDirector是个抽象类,我们知道,抽象类是不能被实例化的,也就是说,你new XXX是不可能的,编译器直接弹错.所以要想子类拥有父类的初始化功能,那只能另外写一个了.
那我们是如何构建的CCDirector这个单例的呢?

static CCDisplayLinkDirector *s_SharedDirector = NULL;

CCDirector* CCDirector::sharedDirector(void)

{
   if (!s_SharedDirector)
   {
       s_SharedDirector = new CCDisplayLinkDirector();
       s_SharedDirector->init();
   }

   return s_SharedDirector;

}
看见了吧,他其实是new了一个自己的子类,来完成自己的功能的.而这个子类里,就有我们非常重要的一个函数.
在CCDirector的初始化函数里,我们看到了一个熟悉的面孔

bool CCDirector::init(void)

{
   CCLOG("cocos2d: %s", cocos2dVersion());
   .........................................
   // create autorelease pool
   CCPoolManager::sharedPoolManager()->push();

   return true;

}
他在整个内核开始运行之前,就初始化了一个内存管理类.之后,在他的析构函数里

CCDirector::~CCDirector(void)

{
   CCLOG("cocos2d: deallocing CCDirector %p", this);
    .....................
   // pop the autorelease pool
   CCPoolManager::sharedPoolManager()->pop();
   CCPoolManager::purgePoolManager();
    ...........................

}
用管理类执行了一个弹栈操作pop().不过看到这里,我有点不解,pop是弹出当前管理池并clear掉,那如果当前有几个管理池同时存在呢?只弹一次,后面几个怎么办?我们还是慢慢来吧.

purgePoolManager()其实是这样

void CCPoolManager::purgePoolManager()

{
   CC_SAFE_DELETE(s_pPoolManager);

}
他删掉当前单例的指针.这样整个单例所保存的数据都会被删除掉,所以也就不用pop所有的元素了.
然后这个CCDirector 中剩下的唯一跟内存有关的,也是最最重要的函数mainLoop(),从他的名字我们就能看出来他的重要性了--主循环.他是一个纯虚函数virtual void mainLoop(void) = 0,在上面提到的子类CCDisplayLinkDirector中被覆写了.
现在我们来看看这个类

class CCDisplayLinkDirector : public CCDirector

{

p
4000
ublic:
   CCDisplayLinkDirector(void) 
       : m_bInvalid(false)
   {}

   virtual void mainLoop(void);
   virtual void setAnimationInterval(double dValue);
   virtual void startAnimation(void);
   virtual void stopAnimation();

protected:
   bool m_bInvalid;

};
他其实就是覆写了抽象类的几个纯虚函数而已.并且通过注释,我们知道他还有些其他的功能和限制.

1.他负责显示并以一定的频率刷新计时器.

2.计时器和界面绘制都是通过一定的频率是同步进行的.

3.只支持每秒60,30,15帧设置.
这些不属于我们讨论范围,简答提一下,我们只关心重要的mainLoop()

void CCDisplayLinkDirector::mainLoop(void)

{
   if (m_bPurgeDirecotorInNextLoop)
   {
       m_bPurgeDirecotorInNextLoop = false;
       purgeDirector();
   }
   else if (! m_bInvalid)
    {
        drawScene();
    
        // release the objects
        CCPoolManager::sharedPoolManager()->pop();       
    }

}
这里在没有失效的状况下(即m_bInvalid不为真),他会执行管理池中的pop函数.至于何时m_bInvalid为真,其实是在这里

void CCDisplayLinkDirector::stopAnimation(void)

{
   m_bInvalid = true;

}
而上面的条件语句中的这个变量m_bPurgeDirecotorInNextLoop,我们从名字里就能看出来,他是否是结束CCDirector的一个标志.既然是mainLoop,那就一定要Loop起来,而这里并没有看到任何Loop的迹象.于是我在内核中查找一下mainLoop在哪里被用过.

int CCApplication::run()

{
   PVRFrameEnableControlWindow(false);

   // Main message loop:
   MSG msg;
   LARGE_INTEGER nFreq;
   LARGE_INTEGER nLast;
   LARGE_INTEGER nNow;

   QueryPerformanceFrequency(&nFreq);
   QueryPerformanceCounter(&nLast);

   // Initialize instance and cocos2d.
   if (!applicationDidFinishLaunching())
   {
       return 0;
   }
   CCEGLView* pMainWnd = CCEGLView::sharedOpenGLView();
   pMainWnd->centerWindow();
   ShowWindow(pMainWnd->getHWnd(), SW_SHOW);
   while (1)
   {
       if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
       {
           // Get current time tick.
           QueryPerformanceCounter(&nNow);
           // If it's the time to draw next frame, draw it, else sleep awhile.
           if (nNow.QuadPart - nLast.QuadPart >m_nAnimationInterval.QuadPart)
           {
               nLast.QuadPart = nNow.QuadPart;
               CCDirector::sharedDirector()->mainLoop();
           }
           else
           {
               Sleep(0);
           }
           continue;
       }
       if (WM_QUIT == msg.message)
       {
           // Quit message loop.
           break;
       }
       // Deal with windows message.
       if (! m_hAccelTable || ! TranslateAccelerator(msg.hwnd,m_hAccelTable, &msg))
       {
           TranslateMessage(&msg);
           DispatchMessage(&msg);
       }
   }
   return (int) msg.wParam;

}
如此大的一个while(1),就是在这里循环的.这个run又是在哪里运行的呢?大家看工程里的main.cpp

int APIENTRY _tWinMain(HINSTANCE hInstance,
                      HINSTANCE hPrevInstance,
                      LPTSTR   lpCmdLine,
                      int      nCmdShow)

{
   UNREFERENCED_PARAMETER(hPrevInstance);
   UNREFERENCED_PARAMETER(lpCmdLine);

   // create the application instance
   AppDelegate app;
   CCEGLView* eglView = CCEGLView::sharedOpenGLView();
   eglView->setFrameSize(960, 640);
   returnCCApplication::sharedApplication()->run();

}
在这里,WIN32平台下的入口函数中,我们的引擎就已经启动了.其他的功能,是启动的一些初始化工作,以及跨平台的东西,这里不在讨论范围之内,我们只管内存管理的东西.
好,基本的过程我都找到了,现在来理一下自动释放的思路.
  假设我们程序已经运行,并且已经存进指针了.那么mainLoop这个函数,在不受到阻碍的情况下,会一直执行,并且一直执行CCPoolManager::sharedPoolManager()->pop().这里我们再把这个pop搬出来看看.还有一个附带的clear().

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);
   }

}

void CCAutoreleasePool::clear()

{
   if(m_pManagedObjectArray->count() > 0)
   {
       //CCAutoreleasePool* pReleasePool;
       CCObject* pObj = NULL;
       CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
       {
           if(!pObj)
               break;
       --(pObj->m_uAutoReleaseCount);
        }
       m_pManagedObjectArray->removeAllObjects();
   }

}

1.如果没有有释放池,就不做任何事,我们有池,一开始就push了一个进去.

2.记录当前池中的指针个数,假设我们有3个.在栈中的顺序以及引用计数分别为4(最后一针),2(第二针),1(第一针);

3.清除掉当前池中的指针.释放的过程是,遍历池中每一个指针,将他们的自动释放计数减一,然后removeAllObject.看看removeAllObjects()干了些什么事吧.

void CCArray::removeAllObjects()

{
   ccArrayRemoveAllObjects(data);

}
额,他其实只是调用了一下ccArray的函数而已,而里面的参数data,就是ccArray结构体的指针,也就是我们当前的自动释放池.

/** Removes all objects from arr */

void ccArrayRemoveAllObjects(ccArray *arr)

{
   while( arr->num > 0 )
   {
       (arr->arr[--arr->num])->release();
   }

}
这个函数写着,从数组里移除所有的指针.其实不算是移除,不算是真正的移除,他只是遍历了一次储存指针的内存地址(即储存内存地址的内存块),并分别执行了一次release操作,即将我们指针的引用计数减1,如果减一之后为0了,那么就删除(delete)
1c140
这个指针.

我来模拟一下他的运行过程:

1.找到第一个引用计数为4的指针(最后一针),将他的释放计数减1,变为0;然后找到第二针,释放计数减1,变为0;最后找到第一针,释放计数减1,变为0. 

2.执行removeAll操作,将栈中每一个指针的引用计数减1.找到最后一针,4->3,第二针2->1,第一针1->0,然后第一针执行delete this操作.即立刻执行析构函数.

CCObject::~CCObject(void)

{
   // if the object is managed, we should remove it
   // from pool manager
   if (m_uAutoReleaseCount > 0)
   {
       CCPoolManager::sharedPoolManager()->removeObject(this);
   }
   // if the object is referenced by Lua engine, remove it
    .....................

}
不过因为m_uAutoReleaseCount已经变成0了,就等于什么都不做,只是例行C++内核过程.那前面一篇提到的m_uAutoReleaseCount什么时候会大一1,这个经过我的研究发现,他不能大于1,他只有0和1两个值,你把它定成bool类型的也可以,只是不方便运算.

呵呵,重点来了.别以为delete干了件跟牛X的事,其实很多初学者都被他的名字骗了.delete其实只干了一件事,那就是把指针指向的内存中的数据给删了,但是指针并没有消失.他任然存在.这就是传说中的野指针,瞎指!,如果使用它,可能就会出现莫名其妙的BUG.所以,一般delete后,都要将指针的赋值为NULL,让系统知道,这指针暂时没用了.不过这里是delete this,this这个指针是极平常又不平常的一个东西,每个类生成的时候,都会自动带一个this指针.this指向的其实就是这个类自己,那delete this就等于"自杀".C++中,允许一个类通过成员函数申请"自杀操作",不过你必须保证,这个类是new出来的,不是对象,而且"自杀"是并且必须是这个类执行的最后一个操作,之后不在对这个类做任何有关数据,以及检查指针等操作.(想对C++有更深的了解,大家可以去看侯捷先生的<<深度探索C++对象模型>>)那你说,我把delete this放在析构里就行了.NO,这是绝对不行的,delete this之后的第一个操作就是执行析构函数,如果再放进去,会形成代码无限循环,导致堆栈溢出.

这样看来,我们保存指针数组中,任然有3个指针存在,虽然有个已经废了........

3.mainLoop再运行一次,然后pop在次执行.不过看到这里我出现了疑问,因为前一次的pop操作,仅仅只是释放了指针所指向的内存,但并没有把指针赋值为NULL,所以,如果再次操作指针,岂不是变成操作野指针了!来吧,再看看代码.
通过把输出数据打印到控制台,我发现了一个现象.

CCPoolManager::pop 1

CCAutoreleasePool::clear 4

0

1

2

3

..................

CCPoolManager::pop 1

CCAutoreleasePool::clear 0
也就是说,在pop一次之后,当前释放池中所储存的指针全部被移除了,不是删除指针,只是从数组中把它们去除.

注意,以下为我个人分析过程中非常重要的一段!!!

这里我要重要提示一下,这里卡了我很久很久,也废了我很多的时间!大家还记得那个自动释放的计数么?就是这玩意m_uAutoReleaseCount,他在add中被加了1,在clear中被减一.开始我的想法是,这个值只要是1,就说明他被加入了自动管理池中管理,为0表示没有加入自动管理池.但是我发现我错了,我找遍了整个内核,都没有找到有哪个地方在判断这个值是否为1(除了CCObject的析构,不过那里没有实际意义).

也就是说,按照我推理的思路,在pop中清理一次指针后,你得把指针移除吧,所以我想到了removeXX函数,可是他们一次也没有被执行.但是,上面的信息中却显示了释放池中没有元素了.那是怎么释放的呢?我这时想到了那个释放计数,他们不是在clear中被归零了么,应该有个判断,找到归零的指针就删除,可惜我错了,我愣是没找到这样的或类似的方法.那这数组究竟是如何变成0的呢.

其实秘密在这里.还记得上面自动释放池执行的那个函数么?在clear中的removeAllObjects,他最终执行的函数是ccArrayRemoveAllObjects,而这个函数干的事我们都知道,那就是

   while( arr->num > 0 )
   {
       (arr->arr[--arr->num])->release();
   }

等一下,这里有个非常阴险的地方,不注意看完全看不出来,我就是没注意,就是这个东西!!!!--arr->num!!!!.最开始我仅仅认为这是循环遍历数组执行release操作.天哪,当我分析了一遍又一遍时,才发现,这就是让自动释放池数组变成0的原因所在!

内核作者并没有真正的把数组释放掉,而是强行把数组的元素个数归零了!!!!!

这句判断if(m_pManagedObjectArray->count()> 0),其中的count()也就是获得数组元素个数的函数,他的原型是这样的.

unsigned int CCArray::count()

{

   return data->num;

}

其中的data就是一个指向ccArray的指针,而ccArray结构体中的第一个参数就是num.

各位肯定还记得我说过的,只delete指针不赋值NULL,是没办法真正删除一个指针的,而他会成为野指针.作者仅仅执行了delete this,没有赋值NULL,clear中却还继续对野指针进行操作,但是整个引擎却没有出现丝毫的BUG.也就是这个原因,让我纠结了很久,以至于发现了如此坑爹的删除数组方式.

这里给大家介绍一下m_uAutoReleaseCount这个自动释放计数的前身.Cocos2d-x这个引擎其实是来源于Cocos2d,而这个引擎是苹果机专用,也就是用的Object-C,而带引用计数的指针是Object-C语言层面就支持的东西,在苹果里面,这个释放计数其实是布尔值. .而C++语言层面并不支持释放计数,也就是带引用计数的指针,所以只能模拟.这下好了,一模拟就模拟了个我目前为止觉得是不仅废材还费脑子的变量m_uAutoReleaseCount.我曾经试图控制他的值,不过引擎会报错.但是我实在是没找到有哪里在判断他的值了.除了那个无关紧要的~CCObject.求高手解答吧!

也就是说,按照内核的自动管理措施,确实可以释放掉不用的内存,但是,会生成一大堆的野指针.真是不负责任的自动管理.不过通过我简单的研究,这个自动管理类,确实没办法将野指针消除,只能让系统回收,算是他引擎的缺陷吧.要不然,Cocos2d的开发者也不会叫我们尽量不要用自动释放.

好啦,重要的分析结束啦!

下面呢,我就把整个自动管理的过程串起来,给大家一个清晰的思路了.

我们首先new一个类出来  CCSprite *pSprite = CCSprite::create(),这个静态工厂方法里,直接就执行了autorelease操作了.

,这里面的过程就是这样的:new操作将引用计数初始化为1,释放计数初始化为0, autorelease通过几次操作,将我们的pSprite,也就是他的内存地址存放进了自动释放池里.然后retain将引用计数加1.这时引用计数为2.这时,我们就可以理解,为什么后面有一个pObject->release()操作了.我们只是想把他加入自动管理,但并不想retain他.于是乎,引用计数还是1.

这时,我们执行addChild(pSprite),将我们的精灵加入节点中,这个操作也会将引用计数加1.来给大家看看代码.

……………………..

,我只挑重要的函数讲,这里执行了一个insertChild操作.

void CCNode::insertChild(CCNode* child, int z)

{

   m_bReorderChildDirty = true;

   ccArrayAppendObjectWithResize(m_pChildren->data, child);

   child->_setZOrder(z);

}

,我们上一篇才提到的函数ccArrayAppendObjectWithResize又出现了,他的出现,就意味着retain的出现.我就不继续粘代码了,那这里的m_pChildren就一定是一个CCArray的指针了.这时pSprite的引用计数为2.

假设这时我们的游戏中有且仅有一个精灵,也就是我们的pSprite,我们暂时把他看成是玩家,游戏中,玩家是我们主要操作的对象,加入自动释放后,就有被自动释放的可能,但是这是我们所不允许的.引擎自然也知道则一点.所以,这时候mainLoop执行一次,pop执行一次,我们当前有自动释放池,所以clear执行一次.clear中,释放计数被减1,归零,然后由于removeAllObjects的执行,我们的玩家pSprite执行一次release,引用计数减由2变成1.然后数组被强行归零.这时mainLoop再次执行的话,释放池中的元素个数就为0了(没有再添加其他的东西).

,这不是坑爹吗?释放池就是用来释放指针的,但是pSprite的引用计数还有1,是不会执行delete this操作的.你说对了,这就是为什么,我们还能操作玩家的原因,如果这个指针被释放了,我们岂不是在操作野指针了?那如何控制玩家?

其实说到这里,大家应该明白了.这个自动释放池,做的事情,仅仅是释放掉引用计数为1的指针.只要引用计数超过1,那就不会释放.做的只是将引用计数减一的操作.那我们结束游戏,或者切换场景时,那些引用计数大于1的指针,改如何释放呢? 分析到这里的,我个人也认为,这个内存管理算是比较坑的了.

仔细想一下,内存泄露的原因是什么,说简单点,就是只new而不delete.什么情况下会发生这样的情况呢?比如说,我们的一个射击游戏,要发射很多子弹,于是我们在一个界面里new很多子弹出来,但是子弹碰撞后不delete掉,以至于子弹越来越多,内存越占越大.然后我们突然切换场景Scene,因为场景的对象消亡了,所以场景的析构函数被执行,自动释放了他占的内存,但是我们的子弹并没有执行析构函数,所以一直把空间占着,那段内存就废掉了,于是乎内存泄露.

但是我们切换界面的时候,玩家的引用计数为1,并且不再释放池内了,那该如何释放?这里,我们就要看下CCNode的析构函数了.

CCNode::~CCNode(void)

{

    CCLOGINFO("cocos2d: deallocing" );

……………………………

    if(m_pChildren&& m_pChildren->count() > 0)

    {

        CCObject*child;

       CCARRAY_FOREACH(m_pChildren, child)

        {

            CCNode*pChild = (CCNode*) child;

            if(pChild)

            {

               pChild->m_pParent = NULL;

            }

        }

    }

    // children

   CC_SAFE_RELEASE(m_pChildren);

}

他帮我们完成了这一步.我们的玩家不是在Layer上的么?Layer又在Scene上,当Scene被替换掉时,会自动执行他的析构函数,再执行父类的析构函数,也就是上面这段,这其中的m_pChildren中就保存着指向Layer的指针,他将Layer的父类赋值为空,然后release掉m_pChildren.而这个m_pChildre是指向CCArray的指针, CCArray是CCObject的子类,初始化时引用计数被加了1,然后autorelease加入自动释放池. m_pChildren被初始化为NULL,他是这样创建的

void CCNode::addChild(CCNode *child,int zOrder, int tag)

{   

   if( ! m_pChildren )

   {

       this->childrenAlloc();

}

}

 

void CCNode::childrenAlloc(void)

{

   m_pChildren = CCArray::createWithCapacity(4);

   m_pChildren->retain();

}

一来就被retain一次,经过一次pop引用计数为1,所以不会被释放.而最后的CC_SAFE_RELEASE(m_pChildren),将他的引用计数变为0.执行delete this操作,进而执行析构函数.

CCArray::~CCArray()

{

   ccArrayFree(data);

}

析构函数里执行了一次释放操作.

void ccArrayFree(ccArray*& arr)

{

   if( arr == NULL ) 

   {

       return;

   }

         ccArrayRemoveAllObjects(arr);

         

         free(arr->arr);

         free(arr);

 

   arr = NULL;

}

这里又执行了一个remove操作,这个蒙了我很久的函数ccArrayRemoveAllObjects(arr),他用m_pChildren数组里的成员执行一次release操作,也就是layer->release(),因为我们的layer并没有手动retain过.所以他的引用计数减1变为0,然后执行delete this.回收内存.接着,保存layer的这个数组被free掉,然后m_pChildren被free掉,接着赋值为NULL,彻底删除指针.

这样一来,layer就彻底没有了.我们以此类推,存在layer上的东西,也就是储存在layer里m_pChildren中的什么CCSprite ,CCMenu,CCLabelTTF等等,都会在界面切换时被彻底删除掉.所以,内存管理不仅仅只是autorelease做的事情,节点CCNode其实承担了相当大一部分内存管理工作.相比起来,释放池做的工作,仅仅是担心我们使用局部指针变量时,忘记release的一种防范策略.

不过这也提醒了我们,如果我们new了一个局部的指针,并且手动retain了一下,那就必须在必要的地方手动release他一次,并且两个操作的次数必须一样.为什么呢?回顾一下上面分析的就知道了,最后仅仅只是release了一下而已,也就是说在不手动retain的情况下,我们的内存管理,最多能回收掉引用计数为2的指针,如果你手动retain了,那最后的那个release不足以把引用计数减到0,那么就内存泄露了………..

不过如果你执行的是退出游戏,那就无所谓了,现在的操作系统,都能在程序退出时,将他所占用的内存全部回收掉,就算是你new了一堆东西出来还不delete.

Cocos2d-x的内存管理到这里就分析完了,虽然没有我想想的那样智能,但是也让我学到很多内存管理的思想.我们下一篇内核分析再见~

后记:

        写完这篇文章后,我在做开发时又想了一下,其实这个自动释放池不能算是坑,他的目的是把我们new出来的指针的引用计数释放到1,从而让Scene切 换,Layer切换,退出程序时,真正的内存管理,也就是CCNode和CCObject的内存管理能顺利吧所有的指针都释放掉.

        所以总结一下,此内存管理的思路是,用autorelease将指针的释放计数控制在一定范围内(最大值是2,autorelease之后必须最大是1),以至于当界面切换,各个类执行remove操作,程序退出等情况下能将占用的内存全部释放掉.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  cocos2d-x