Cocos2d-X内存管理研究<一>
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++,这样形成了一个顺序储存的数组.
总结一下,他费了半天劲,其实就是要保管一堆内存地址罢了.
如果想做自己的内存管理,就可以学习他的思想,保管指针地址.
如何动态释放这些指针呢,等下一篇再叙.