Cocos2d-X内存管理研究

半夜没事干,研究内核,作为我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++,这样形成了一个顺序储存的数组.
至此,如何将指针加入管理类的原理,我们就基本看完了.
总结一下,他费了半天劲,其实就是要保管一堆内存地址罢了.
如果想做自己的内存管理,就可以学习他的思想,保管指针地址.
如何动态释放这些指针呢,等下一篇再叙.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值