Cocos2d-x的内存管理
要是完全没有接触过Objc, 只是了解C++, 看到cocos2d-x的内存管理设计, 会想说脏话的. 了解objc的话, 起码还能理解cocos2d-x的开发者是尝试在C++中模拟Objc的内存管理方式. 不仅仅是说加引用计数而已, 因为真要在C++中加引用计数的方法有很多种, cocos2d-x用的这种方法, 实在太不原生态了.
简单情况
因为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的代码就不行.
容器对引用计数的影响
这个部分是引用计数方法都会碰到的问题, 也就是引用计数到底在什么时候增加, 什么时候减少.
在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是对引用计数有影响的.
见下面的代码:
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++的容器, 写了下面这样的代码: (未编译测试, 纯示例使用)
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函数.
内存池
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对游戏对象有了所有权以后, 又能正确的保证该对象不会被删除.
小结
本文原来是来自于给公司做的内部培训材料, 因为一开始写的很初略和简单, 一直就没想发布, 最近我在整理老的资料, 所以今天整理了一下, 添加了一些例子, 发布出来了, 可以明显的看到后面的内容虽然更加重要, 但是写的比前面要仓促, 有错误的话, 请各位不吝赐教.