要是彻底没有接触过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对游戏对象有了全部权之后, 又能正确的保证该对象不会被删除.
本文原来是来自于给公司作的内部培训材料, 由于一开始写的很初略和简单, 一直就没想发布, 最近我在整理老的资料, 因此今天整理了一下, 添加了一些例子, 发布出来了, 能够明显的看到后面的内容虽然更加剧要, 可是写的比前面要仓促, 有错误的话, 请各位不吝赐教.