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

Cocos2d-X内存管理研究<一>

2013-07-05 20:09 344 查看



来自特兹卡特的百度空间

http://hi.baidu.com/tzkt623/item/651ca7d7a0aff6e055347f67



半夜没事干,研究内核,作为我cocos2d-x的第一篇教程.cocos2dx是一个树形结构的引擎,具体结构我暂时不分析,这里只讲内存管理.网上的分析都是说个纯理论,我深入代码内核,给大家详细讲解.

最开始我们寻找一下这棵树的最大的根节点CCZone.

class CC_DLL CCZone

{

public:

CCZone(CCObject *pObject = NULL);

public:

CCObject *m_pCopyObject;

};

他其实没干什么事,就是一个简单的赋值.

CCZone::CCZone(CCObject *pObject)

{

m_pCopyObject = pObject;

}

将当前的CCObjec付给自己的委托变量CCObject *m_pCopyObject.

然后我们来看CCObject.

class CC_DLL CCObject : public CCCopying

{

public:

// object id, CCScriptSupport need public m_uID

unsigned int m_uID;

// Lua reference id

int m_nLuaID;

protected:

// count of references

unsigned int m_uReference;

// count of autorelease

unsigned int m_uAutoReleaseCount;

public:

CCObject(void);

virtual ~CCObject(void);

void release(void);

void retain(void);

CCObject* autorelease(void);

CCObject* copy(void);

bool isSingleReference(void);

unsigned int retainCount(void);

virtual bool isEqual(const CCObject* pObject);

virtual void update(float dt) {CC_UNUSED_PARAM(dt);};

friend class CCAutoreleasePool;

};

他干的事有点多,不过先不管,我们看他的父类CCCopying.

class CC_DLL CCCopying

{

public:

virtual CCObject* copyWithZone(CCZone* pZone);

};

这个类也没干什么事,只是定义了一个返回类型为CCObject指针的虚函数.

来看实现

CCObject* CCCopying::copyWithZone(CCZone *pZone)

{

CC_UNUSED_PARAM(pZone);

CCAssert(0, "not implement");

return 0;

}

直接返回了0,看似没什么作用,断言也规定了必须是0.........pZone参数未使用,这让我想起了这个函数的调用方法,可能是传他的函数地址到一个参数中.

好了,现在看来,内存管理跟前面的父类关系不是很大,那我们直接看CCObject的成员函数.

public:

CCObject(void);

virtual ~CCObject(void);

void release(void);

void retain(void);

CCObject* autorelease(void);

CCObject* copy(void);

bool isSingleReference(void);

unsigned int retainCount(void);

friend class CCAutoreleasePool;

这几个成员函数以及一个名叫CCAutoreleasePool的友元类是比较重要的东西了,我们一个一个看.

先看构造函数.

CCObject::CCObject(void)

:m_uAutoReleaseCount(0)

,m_uReference(1) // when the object is created, the reference count of it is 1

,m_nLuaID(0)

{

static unsigned int uObjectCount = 0;

m_uID = ++uObjectCount;

}

将他m_uAutoReleaseCount的计数初始化为0,并将m_uReference引用计数初始化为1.m_nLuaID这个不在C++范围之内,暂时不管.在函数内,他给一个静态无符号整形计数uObjectCount赋值为0,并将m_uID赋值,不过这个我们不关心.

析构函数东西有点多,我只讲重点

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,就把这个类从管理池中删除.那我们可以猜想,只要m_uAutoReleaseCount参数大于0,那么就说明此类被加入了内存管理系统,以至于m_uAutoReleaseCount是如何大于0的,大于1又会是什么样的情况,后面在看.

接下来是release()

void CCObject::release(void)

{

CCAssert(m_uReference > 0, "reference count should greater than 0");

--m_uReference;

if (m_uReference == 0)

{

delete this;

}

}

他就是把引用计数减一,如果引用计数为0了,那么就删掉他.这里我们可以猜想,引用计数有可能大于1,至于为什么会大于1,慢慢看.

现在是retain()

void CCObject::retain(void)

{

CCAssert(m_uReference > 0, "reference count should greater than 0");

++m_uReference;

}

他就是把引用计数加1,正好也解释了引用计数为什么会大于1的情况.初始化类成功之后,引用计数为1,如果再retain一下,就大于1了.

接下来是比较重要的函数autorelease()

CCObject* CCObject::autorelease(void)

{

CCPoolManager::sharedPoolManager()->addObject(this);

return this;

}

他把当前类加入管理池,返回一个被加入管理池中的指向CCObject的指针.也就是返回当前指针.

好了,后面的暂时不看了,我们找到了比较重要的东西了,这个CCPoolManager在内存管理里面扮演了重要的角色,我们现在去研究它.

class CC_DLL CCPoolManager

{

CCArray* m_pReleasePoolStack;

CCAutoreleasePool* m_pCurReleasePool;

CCAutoreleasePool* getCurReleasePool();

public:

CCPoolManager();

~CCPoolManager();

void finalize();

void push();

void pop();

void removeObject(CCObject* pObject);

void addObject(CCObject* pObject);

static CCPoolManager* sharedPoolManager();

static void purgePoolManager();

friend class CCAutoreleasePool;

};

首先,他不继承自任何类,说明他是老大级的人物了,我们应该好好研究一番了.

不过一上来就看到三个委托.

CCArray* m_pReleasePoolStack;

CCAutoreleasePool* m_pCurReleasePool;

CCAutoreleasePool* getCurReleasePool();

这下有得看了.我们先知道他们存在就行了.还是先研究成员函数.

由于整个系统中,内存管理有并且只需有一个就够了,所以这个类是个单例.什么是单例我就不说了.自己了解.

CCPoolManager::CCPoolManager()

{

m_pReleasePoolStack = new CCArray();

m_pReleasePoolStack->init();

m_pCurReleasePool = 0;

}

构造函数里,做了3件事,m_pReleasePoolStack参数new了一个CCArray出来,并且初始化了一下,意会一下他的名字,释放池栈.然后给 m_pCurReleasePool这个指针初始化为0,说明当前还没有自动内存管理的池.不过这里我有点不明白,就是init().在CCArray()的构造函数里已经调用过一次,为何还来一次,难道有BUG?

接下来是析构

CCPoolManager::~CCPoolManager()

{

finalize();

// we only release the last autorelease pool here

m_pCurReleasePool = 0;

m_pReleasePoolStack->removeObjectAtIndex(0);

CC_SAFE_DELETE(m_pReleasePoolStack);

}

析构里调用了一个finalize(),并且说,只release最后一个自动管理池(第一个进栈的).那我们先不管,来看看这个finalize()

void CCPoolManager::finalize()

{

if(m_pReleasePoolStack->count() > 0)

{

//CCAutoreleasePool* pReleasePool;

CCObject* pObj = NULL;

CCARRAY_FOREACH(m_pReleasePoolStack, pObj)

{

if(!pObj)

break;

CCAutoreleasePool* pPool = (CCAutoreleasePool*)pObj;

pPool->clear();

}

}

}

他做的事就是如果自动释放的池中有东西,那就全部clear掉.这个clear是个什么,暂时不管,我们先把其他的看完.

管理池最大的作用就是管理添加到他里面的指针,所以我们先看看添加函数

void CCPoolManager::addObject(CCObject* pObject)

{

getCurReleasePool()->addObject(pObject);

}

他说,我其实只是把你们传进来的指针加在当前的释放池里了.那这个getCurReleasePool()又是个什么玩意.

CCAutoreleasePool* CCPoolManager::getCurReleasePool()

{

if(!m_pCurReleasePool)

{

push();

}

CCAssert(m_pCurReleasePool, "current auto release pool should not be null");

return m_pCurReleasePool;

}

他是一个返回类型为指向CCAutoreleasePool的指针的函数,他干了什么呢?如果当前没有创建释放池,那么push()一个进去.并且断言释放池必须有.最后返回这个自动释放池的指针.

那么我们猜也能猜到push()干了什么了,无非就是new了一个CCAutoreleasePool出来.

void CCPoolManager::push()

{

CCAutoreleasePool* pPool = new CCAutoreleasePool(); //ref = 1

m_pCurReleasePool = pPool;

m_pReleasePoolStack->addObject(pPool); //ref = 2

pPool->release(); //ref = 1

}

new出来之后,将自动释放池委托给当前管理类,并把它加入了释放池栈中.然后release掉自己.

有push那肯定就有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 = NULL;*/

}

他 的功能是,如果当前没有释放池,那就什么事也不干return掉.如果有值,记录下当前总共有多少个释放池.并且clear掉当前释放池.如果当前释放池 的数量大于1,那么,移除最后一个释放池.为什么是最后一个,因为管理池是个栈,先进后出,最后进去的是排在出口第一个位置,并且计算机都是以0开始计数 的,所以在减1才是最后一个位置的元素.然后把栈中倒数第二个元素(弹栈后的当前池)赋给当前管理池的参数m_pCurReleasePool.

那么,现在该removeObject了

void CCPoolManager::removeObject(CCObject* pObject)

{

CCAssert(m_pCurReleasePool, "current auto release pool should not be null");

m_pCurReleasePool->removeObject(pObject);

}

他只是把当前传入池中的指针变量移除.

CCPoolManager看完了,这里最重要的就是CCAutoreleasePool,那么我们转去看他.

与管理类比起来,释放池就简单多了.

class CC_DLL CCAutoreleasePool : public CCObject

{

CCArray* m_pManagedObjectArray;

public:

CCAutoreleasePool(void);

~CCAutoreleasePool(void);

void addObject(CCObject *pObject);

void removeObject(CCObject *pObject);

void clear();

};

不过他却继承自CCObject.这是为什么,至少目前我们可以看出来有一点,在CCPoolManager::push()中他用到了release(),而这个函数是CCObject中定义的,要用它继承是个好办法.不过在CCObject中已经申明了CCAutoreleasePool为他的友元类了,就可以完全访问CCObject中所有的数据.这里又继承一下,是什意思?还记得上面的一段代码么m_pReleasePoolStack->addObject(pPool)对,他要自己管理自己,所以得继承自CCObject,但是CCObject无法把将自己的私有成员继承给他,所以只能友元解决.

CCAutoreleasePool::CCAutoreleasePool(void)

{

m_pManagedObjectArray = new CCArray();

m_pManagedObjectArray->init();

}

此类构造函数很简单,也是new了一个CCArray()出来,然后init()一下.

CCAutoreleasePool::~CCAutoreleasePool(void)

{

CC_SAFE_DELETE(m_pManagedObjectArray);

}

析构删除它.

然后就是我们见过很多次但从未见过真身的addObject

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.

}

他将当前指针加入一个CCArray数组中.并且断言引用计数必须大于1.并且将自动释放计数加1,让其受到自动释放池的管理.还记得上面说到m_uAutoReleaseCount怎样才会大于0么,这里就揭示是原因所在.最后竟然release了一次指针.后面写着,这里的引用计数应该是0,但是加入自动释放池时加了1.这是怎么加的1?然后还有引用计数如何大于1的?我们先不着急,看完其他函数再来研究.

下一个自然就是remove了

void CCAutoreleasePool::removeObject(CCObject* pObject)

{

for (unsigned int i = 0; i < pObject->m_uAutoReleaseCount; ++i)

{

m_pManagedObjectArray->removeObject(pObject, false);

}

}

这个函数是遍历所有释放计数,然后remove掉所有元素,这里的removeObject(pObject, false)是CCArray中的函数,我们暂时不管.

最后一个函数clear

void CCAutoreleasePool::clear()

{

if(m_pManagedObjectArray->count() > 0)

{

//CCAutoreleasePool* pReleasePool;

#ifdef _DEBUG

int nIndex = m_pManagedObjectArray->count() - 1;

#endif

CCObject* pObj = NULL;

CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)

{

if(!pObj)

break;

--(pObj->m_uAutoReleaseCount);

#ifdef _DEBUG

nIndex--;

#endif

}

m_pManagedObjectArray->removeAllObjects();

}

这里是,如果指针管理数组里有东西,那就遍历所有的指针,将释放计数减到0,最后删掉所有数组中的东西.

自 此,cocos2d-x的内存管理类就全部浏览完毕了,除了一个CCArray,不过通过名字,和他的作用,我们就能清楚的知道,他一定是一个继承自 CCObject的数组,否者是不能存放CCObject类型的指针的.不过这个不重要,这套内存管理是如何运行的,还有上面的疑问到底是怎么回事才是最 重要的.

接下来我们就来理一下这个内存管理的思路吧.

1.由于引擎是树状的,那么我们每new一个类出来,也就是没生成一个指针,就会调用它所有父类的构造函数一次.于是乎,CCObject这个最大的节 点,每次都会执行一次构造函数,将3个参数初始化.并且给m_uID赋值,由于uObjectCount是静态无符号整形,那么就说明每一个新new出来 的节点,都有自己唯一的ID,所以我们写程序的时候,最好不要去修改m_uID这个参数,虽然他是public,因为当东西多了之后,难免会出现BUG.

2.我们将new出来的指针,执行autorelease()操作,也就是把当前new出来的指针加入了内存池管理类CCPoolManager和自动释放类CCAutoreleasePool中.放入其中时,其实只执行了一个函数,CCAutoreleasePool中的addObject,他的作用就是把释放计数加1,但是这里断言引用计数必须大与1,并且通过控制台,我发现他确实大于1.但是new出CCObject时,引用计数只是1,那这增加引用计数的地方在哪呢?

通过注释我们可以发现,每addObject一次,引用计数就会被加1.那么,就一定是这个add干的事.addObject是CCArray的方法,我们转到CCArray中查看,发现他其实是这样的.

void CCArray::addObject(CCObject* object)

{

ccArrayAppendObjectWithResize(data, object);

}

他也只干了一件是,就是生成一个指定的大小的ccArray,注意这里是小写的,这个ccArray是C语言写的,他只是一个结构体.

typedef struct _ccArray {

unsigned int num, max;

CCObject** arr;

} ccArray;

那这个CCObject** arr变量是什么意思呢.我 们知道X *p是指针,那X **p,就是指向指针的指针,统称多级指针.怎么理解呢,我们都知道,指针指向的是内存地址,当我们需要运用哪一块内存中的内容时,指针就指向那一块内存 地址,以此提取出内存中的数据来用.那么指向指针的指针其实就可以这样理解:还存在一个指针,他指向我们当前使用的指针,这个指针指向的内存中所保存的数 据,是我们当前使用的指针指向的内存地址.

这里为什么要这样声明,从上面的自动释放类中,我们可以得到启示.自动释放类保管的是函数指针,而这么多的指针,是通过一个可扩大的动态数组来保管,那么这个数组的本质,就是保管的一堆内存地址.如何保管内存地址呢?多级指针就可以帮你完成.

/** Appends an object. Capacity of arr is increased if needed. */

void ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)

{

ccArrayEnsureExtraCapacity(arr, 1);

ccArrayAppendObject(arr, object);

}

这个函数就是ccArray中的函数了,附加一个对象,如果需要,数组大小可以动态扩展.细心的朋友可能发现了,这个函数没有作用域!!也就是没有前面的XXXX::这一段.这就表明他是一个全局函数,C语言中,没有类的概念,自然都是全局函数.那么我们一个一个看.

void ccArrayEnsureExtraCapacity(ccArray *arr, unsigned int extra)

{

while (arr->max < arr->num + extra)

{

ccArrayDoubleCapacity(arr);

}

}

从他的名字我们就能看出来他的功能,确定分配额外的大小.如果数组最大的大小小于数组元素个数加额外空间的大小,那就分配双倍的数组空间.

void ccArrayDoubleCapacity(ccArray *arr)

{

arr->max *= 2;

CCObject** newArr = (CCObject**)realloc( arr->arr, arr->max * sizeof(CCObject*) );

// will fail when there's not enough memory

CCAssert(newArr != 0, "ccArrayDoubleCapacity failed. Not enough memory");

arr->arr = newArr;

}

这么一来,先将数组最大空间变成双倍.然后新建一个CCObject** newArr.执行realloc,他是一个C语言函数.

给大家看一下他的原型:void *realloc(void *mem_address, unsigned int newsize);

用法:指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)

这样,我们就把一个保管指向CCObject类指针内存地址的内存块,扩大了两倍.然后返回这个内存块的地址.

接下来的才是重头戏.

/** Appends an object. Behavior undefined if array doesn't have enough capacity. */

void ccArrayAppendObject(ccArray *arr, CCObject* object)

{

CCAssert(object != NULL, "Invalid parameter!");

object->retain();

arr->arr[arr->num] = object;

arr->num++;

}

他说,附加一个对象,如果数组大小不够了的话,会发生未知的行为...............真坑爹......不过这里,我们见到了我们一直想见的东西.retain()终于出现了,现在,我们就可以解释,为什么m_uReference会大于1了.arr->arr[arr->num] = object是什么意思呢?还是多级指针问题,arr 是指向储存指针(实为内存地址)的内存.这里要牵涉到数组了,其实一位数组等价于,指向一段连续储存的内存首地址的指针,即我们使用a[3]时编译器自动 会将其变成指针运算*(a + 3),其实3后面还隐藏了东西,是*
sizeof(type),这里是内存寻址原理,首地址加上偏移量,等于当前想找的内存地址,偏移量就是数据类型的大小,比如int为4个字节,那么每块 内存数据块的大小就是4个字节,如果总共有16字节,那么就是储存了4个数据块,每4字节做为偏移.

所以这里也是一样的,编译器自动把他变成*(arr + arr->num),意思是,找到这个内存块指向的地址,这里面准备装的是我们new出来的指针的内存地址,所以,就把object,也就是我们add进去的指针的内存地址放了进去,然后num++,这样形成了一个顺序储存的数组.

至此,如何将指针加入管理类的原理,我们就基本看完了.

总结一下,他费了半天劲,其实就是要保管一堆内存地址罢了.

如果想做自己的内存管理,就可以学习他的思想,保管指针地址.

如何动态释放这些指针呢,等下一篇再叙.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: