Cocos2d-x的内存管理

Cocos2d-x的内存管理

要是彻底没有接触过Objc, 只是了解C++, 看到cocos2d-x的内存管理设计, 会想说脏话的. 了解objc的话, 起码还能理解cocos2d-x的开发者是尝试在C++中模拟Objc的内存管理方式. 不只仅是说加引用计数而已, 由于真要在C++中加引用计数的方法有不少种, cocos2d-x用的这种方法, 实在太不原生态了.node

简单状况

由于cocos2d-x中牵涉到显示的状况最多, 我也就不拿CCArray这种东西作例子了, 看个CCSprite的例子吧, 用cocos2d-x的XCode template生成的HelloWorld工程中, 删除原来的显示代码, 建立一个Sprite并显示的代码以下:算法

// part code of applicationDidFinishLaunching in AppDelegate.cpp
// create a scene. it's an autorelease object
CCScene *scene = HelloWorld::scene();

CCSprite *helloworld = new CCSprite;
if (helloworld->initWithFile("HelloWorld.png")) {
  CCSize size = CCDirector::sharedDirector()->getWinSize();
  // position the sprite on the center of the screen
  helloworld->setPosition( ccp(size.width/2, size.height/2) );

  // add the sprite as a child to this layer
  scene->addChild(helloworld, 0);
  helloworld->release();
}

这里暂时无论HelloWorld::scene, 先关注CCSprite的建立和使用, 这里使用new建立了CCSprite, 而后使用scene的addChild函数, 添加的到了scene中, 并显示. 一段这样简单的代码, 可是背后的东西却不少, 好比, 为啥我在scene的addChild后, 调用了sprite的release函数呢?
仍是能够从引用计数的全部权上提及(这样比较好理解, 虽然你也能够死记哪些时候具体引用计数的次数是几). 当咱们用new建立了一个Sprite时, 此时Sprite的引用计数为1, 而且全部权属于helloworld这个指针, 咱们在把helloworld用scene的addChild函数添加到scene中后, helloworld的引用计数此时为2, 由helloworld指针和scene共享全部权, 此时, helloworld指针的做用其实已经完了, 咱们接下来也不许备使用这个指针, 全部权留着就再也释放不了了, 因此咱们用release方法特别释放掉helloworld指针此时的全部权, 这么调用之后, 最后helloworld这个Sprite全部权彻底的属于scene.
可是咱们这么作有什么好处呢? 好处就是当scene不想要显示helloworld时, 直接removeChild helloworld就能够了, 此时没有对象再拥有helloworld这个sprite, 引用技术为零, 这个sprite会如期的释放掉, 不会致使内存泄漏.
好比说下列代码:安全

// create a scene. it's an autorelease object
CCScene *scene = HelloWorld::scene();

//  CCSprite* sprite = CCSprite::create("HelloWorld.png");
CCSprite *helloworld = new CCSprite;
if (helloworld->initWithFile("HelloWorld.png")) {
  CCSize size = CCDirector::sharedDirector()->getWinSize();
  // position the sprite on the center of the screen
  helloworld->setPosition( ccp(size.width/2, size.height/2) );

  // add the sprite as a child to this layer
  scene->addChild(helloworld, 0);
  helloworld->release();

  scene->removeChild(helloworld);
}

上面的代码helloworld sprite能正常的析构和释放内存, 假如少了那句release的代码就不行.app

容器对引用计数的影响

这个部分是引用计数方法都会碰到的问题, 也就是引用计数到底在何时增长, 何时减小.
在cocos2d-x中, 我却是较少会像在objc中手动的retain对象了, 主要的对象主要由CCNode和CCArray等容器管理. 在cocos2d-x中, 以CC开头的, 模拟Objc接口的容器, 都是对引用计数有影响的, 而原生的C++容器, 对cocos2d-x的对象的引用计数都没有影响, 这致使了人们使用方式上的割裂. 大部分用惯了C++的人, 估计都仍是偏向使用C++的原生容器, 毕竟C++的原生容器及其配套算法算是C++目前为数很少的亮点了, 比objc原生的容器都要好用, 更别说Cocos2d-x在C++中模拟的那些objc容器了. 可是, 一旦走上这条路就须要很是当心, 要很是明确此时每一个对象的全部权是谁.
看下面的代码:函数

vector<CCSprite*> sprites;
for (int i = 0; i < 3; ++i) {
  CCSprite *helloworld = new CCSprite;
  if (helloworld->initWithFile("HelloWorld.png")) {
    CCSize size = CCDirector::sharedDirector()->getWinSize();
    // position the sprite on the center of the screen
    helloworld->setPosition( ccp(size.width/2, size.height/2) );

    // add the sprite as a child to this layer
    scene->addChild(helloworld, 0);
    sprites.push_back(helloworld);
    helloworld->release();

    scene->removeChild(helloworld);
  }
}

由于C++的容器是对Cocos2d-x的引用计数没有影响的, 因此在上述代码运行后, 虽然vector中保存者sprite的指针, 可是其实都已是野指针了, 全部的sprite实际已经析构调了. 这种状况至关危险. 把上述代码中的vector改为cocos2d-x中的CCArray就能够解决上面的问题, 由于CCArray是对引用计数有影响的.
见下面的代码:oop

CCArray *sprites = CCArray::create();
for (int i = 0; i < 3; ++i) {
  CCSprite *helloworld = new CCSprite;
  if (helloworld->initWithFile("HelloWorld.png")) {
    CCSize size = CCDirector::sharedDirector()->getWinSize();
    // position the sprite on the center of the screen
    helloworld->setPosition( ccp(size.width/2, size.height/2) );

    // add the sprite as a child to this layer
    scene->addChild(helloworld, 0);
    sprites->addObject(helloworld);
    helloworld->release();

    scene->removeChild(helloworld);
  }
}

改动很是小, 仅仅是容器类型从C++原生容器换成了Cocos2d-x从Objc模拟过来的array, 可是这段代码执行后, sprites中的sprite均可以正常的使用, 而且没有问题. 可参考cocos2d-x的源代码ccArray.cpp:测试

/** 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++;
}

可是, 假如我就是想用C++原生容器, 不想用CCArray怎么办呢? 须要承担的风险就来了, 有的时候还行, 好比上例, 我只须要去掉helloworld->release那一行, 而且明白此时全部权已是属于vector了, 在vector处理完毕后, 再release便可.
而有的时候这就没有那么简单了. 特别是Cocos2d-x由于依赖引用计数, 不只仅是addChild等容器添加会增长引用计数, 回调的设计(模拟objc中的delegate)也会对引用计数有影响的. 曾经有人在初学Cocos2d-x的时候, 问我cocos2d-x有没有什么设计问题, 有没有啥坑, 我以为这就是最大的一个.
举个简单的例子, 我真心不喜欢引用计数, 因此全用C++的容器, 写了下面这样的代码: (未编译测试, 纯示例使用)this

class Enemy 
{
  public:
    Enemy() {}
    ~Enemy() {}

};


class EnemyManager 
{
  public:
    EnemyManager() {}
    ~EnemyManager() {}

    void RemoveEnemies() {
      for (auto it : enemies_) {
        delete *it;
      }
    }

private:
  vector<Enemy*> enemies_;
};

刚开始的时候, 这只是一段和Cocos2d-x彻底没有关系的代码, 而且运行良好, 有一天, 我感受的Enmey实际上是个Sprite就方便操做了. 将Enemy改成继承自Sprite, 那么这段代码就没有那么安全了, 由于EnemyManager在彻底不知道enemy的引用计数的状况下, 使用delete删除了enmey, 假如此时还有其余地方对该enemy有引用, 就会crash. 虽然表面上看来是想添加一些CCSprite的显示功能, 可是实际上, 一入此门(从CCObject继承过来), 引用计数就已经无处不在, 此时须要把直接的delete改成调用release函数.spa

内存池

cocos2d-x起始也模拟了objc中的内存池, 可是由于不可能改变语言自己的特性, 那种简单的语法糖语法就没有, 须要的时候, 老实的操做CCPoolManager和CCAutoreleasePool吧. 在一般状况下, cocos2d-x增长的机制使得咱们不太须要像在objc中那样使用内存池. 我来解释一下:
在cocos2d-x中, 几乎全部有意义的类都有create函数, 好比Sprite的create函数:设计

CCSprite* CCSprite::create()
{
    CCSprite *pSprite = new CCSprite();
    if (pSprite && pSprite->init())
    {
        pSprite->autorelease();
        return pSprite;
    }
    CC_SAFE_DELETE(pSprite);
    return NULL;
}

基本只干两个事情, 一个是new和init, 一个就是调用autorelease函数讲sprite自己加入内存池了. 此时讲sprite加入内存池后, sprite的全部权已经属于内存池了, 咱们返回的指针实际上是没有全部权的. 在create出一个相似对象后, 咱们接下来的操做每每是吧这个对象再添加到parent node中(好比上层的scene或layer), 此时由内存池和这个parent node共同拥有这个sprite, 当sprite不须要再显示的时候, 直接经过removeChild将sprite从父节点中移除后, 就回到仅属于内存池的状况了.
在objc中, 要是都是上面的状况, 咱们又不手动的清理内存池, 这其实就已经有内存泄漏了, 可是cocos2d-x实际是每帧都帮咱们清理内存池的. 也就是说, 每一帧仅仅属于内存池的对象都会被释放. 见下面的代码:

void CCDisplayLinkDirector::mainLoop(void)
{
    if (m_bPurgeDirecotorInNextLoop)
    {
        m_bPurgeDirecotorInNextLoop = false;
        purgeDirector();
    }
    else if (! m_bInvalid)
     {
         drawScene();

         // release the objects
         CCPoolManager::sharedPoolManager()->pop();        
     }
}

上面的代码是CCDirector的游戏主循环代码, 主循环干了件很是重要的事情, 那就是pop最上层的autorelease pool, 此时是在release所有仅仅由此内存池全部的对象. 就是依靠这样的原理, 咱们能够放心的将对象放在autorelease pool中, 知道在须要的时候, 这个对象就能正确的释放, 同时只要有上层的父节点经过addChild对游戏对象有了全部权之后, 又能正确的保证该对象不会被删除.

小结

本文原来是来自于给公司作的内部培训材料, 由于一开始写的很初略和简单, 一直就没想发布, 最近我在整理老的资料, 因此今天整理了一下, 添加了一些例子, 发布出来了, 能够明显的看到后面的内容虽然更加剧要, 可是写的比前面要仓促, 有错误的话, 请各位不吝赐教.

相关文章
相关标签/搜索