Cocos2d-x使用的内存管理方式是引用计数,引用计数是一种很有效的机制,通过给每个对象维护一个引用计数器,记录该对象当前被引用的次数。当对象增加一次引用时,计数器加1;而对象失去一次引用时,计数器减1;当引用计数为0时,标志着该对象的生命周期结束,自动触发对象的回收释放。引用计数的重要规则是每一个程序片段必须负责任地维护引用计数,在需要维持对象生存的程序段的开始和结束分别增加和减少一次引用计数,这样就可以实现十分灵活的内存管理。
接下来看一下Cocos2d-x 3.1 版本的源码是怎么实现引用计数的。
一、Ref
我们都知道几乎每个类都继承一个类Ref,打开CCRef.h查看Ref类,去掉其它与引用计数无关的,可以简化为:
// CCRef.h class CC_DLL Ref { public: void retain(); // 引用计数加1 void release(); // 引用计数减1 Ref* autorelease(); // 将对象交给自动释放池 unsigned int getReferenceCount() const; // 获取当前的引用计数 protected: Ref(); // 构造函数,这里的权限为protected,说明自己不能实例化,子类可以实例化 public: virtual ~Ref(); protected: unsigned int _referenceCount; // 引用计数 friend class AutoreleasePool; // 这个先别管 };
// CCRef.cpp Ref::Ref(): _referenceCount(1) // new一个对象时,引用计数加1 { } Ref::~Ref() { } void Ref::retain() { CCASSERT(_referenceCount > 0, "reference count should greater than 0"); ++_referenceCount; } void Ref::release() { CCASSERT(_referenceCount > 0, "reference count should greater than 0"); --_referenceCount; if (_referenceCount == 0) // 引用为0时说明没有调用该对象,此时delete对象 { delete this; } } Ref* Ref::autorelease() { PoolManager::getInstance()->getCurrentPool()->addObject(this); // 交给自动释放池管理 return this; } unsigned int Ref::getReferenceCount() const { return _referenceCount; }
总结一下,Ref类就做了几件事:
1、Ref自己不能实例化,只能由子类实例化;
2、创建是引用计数为1;
3、调用retain引用计数加1;
4、调用release引用计数减1;
5、调用autorelease并没有使引用计数减1,而是交给自动释放池来管理。
那么自动释放池是什么呢?肯定跟autorelease方法里面的PoolManager有关。
打开CCAutoreleasePool.h文件查看,发现有两个类,一个是AutoreleasePool,一个是PoolManager,从字面意思看,AutoreleasePool就是自动释放池,而PoolManager就是池管理器,这些思路有点清晰了:
1、调用autorelease后对象交给AutoreleasePool来管理;
2、PoolManager是用来管理AutoreleasePool的,说明可以有多个池。
二、AutoreleasePool
接下来一步步看,先看AutoreleasePool自动释放池,看简化版本的:
// AutoreleasePool.h class CC_DLL AutoreleasePool { public: AutoreleasePool(); // 疑问1:不要在堆上创建,而应该在栈上创建(为什么呢?等下解释) ~AutoreleasePool(); void addObject(Ref *object); // 添加一个Ref对象到释放池 void clear(); // 清理自动释放池 private: std::vector<Ref*> _managedObjectArray; // 用来保存这个自动释放池里面加入的所有Ref对象 };
// AutoreleasePool.cpp AutoreleasePool::AutoreleasePool() { _managedObjectArray.reserve(150); // 1、设置容器大小为150 PoolManager::getInstance()->push(this); // 2、新建一个释放池时就加入了释放池管理器中(可以暂时放着,等看了PoolManager再回来看) } AutoreleasePool::~AutoreleasePool() { clear(); // 1、清理释放池 PoolManager::getInstance()->pop(); // 2、将释放池从释放池管理器中删除 } void AutoreleasePool::addObject(Ref* object) { _managedObjectArray.push_back(object); // 添加一个Ref对象到释放池中 } void AutoreleasePool::clear() { for (const auto &obj : _managedObjectArray) // 1、调用所有自动释放对象的release函数,注意:只有当引用计数为0时才会delete对象,相同对象加入几次就会release几次 { obj->release(); } _managedObjectArray.clear(); // 2、清除容器 }
总结一下,AutoreleasePool类就做了几件事:
1、维持一个保存Ref对象的队列,这些Ref对象调用autorelease就会加到该队列,调用addObject函数添加;
2、clear函数对AutoreleasePool管理的所有Ref执行一次release操作,只有当引用计数为0时对象才会delete,加入几次就执行几次release操作。
三、PoolManager
PoolManager是管理释放池的,在AutoreleasePool用到push和pop方法,可以猜到PoolManager应该维持一个存放释放池的栈:
// PoolManager.h class CC_DLL PoolManager { public: static PoolManager* getInstance(); // PoolManager是个单例模式 static void destroyInstance(); // 删除单例模式创建的对象 AutoreleasePool *getCurrentPool() const; // 获取当前的释放池 private: PoolManager(); ~PoolManager(); void push(AutoreleasePool *pool); // 压入一个释放池 void pop(); // 弹出一个释放池 static PoolManager* s_singleInstance; std::deque<AutoreleasePool*> _releasePoolStack; // 存放自动释放池的栈 AutoreleasePool *_curReleasePool; // 当前的自动释放池 };
// PoolManager.cpp PoolManager* PoolManager::s_singleInstance = nullptr; // 获取单例模式时,如果还没创建,则会创建两个释放池并添加到池管理器中 PoolManager* PoolManager::getInstance() { if (s_singleInstance == nullptr) { s_singleInstance = new PoolManager(); // 第一个池:AutoreleasePool构造函数会将构造的池添加到池管理器中 s_singleInstance->_curReleasePool = new AutoreleasePool(); // 第二个池:将new出来的释放池再一次压入池管理器中 s_singleInstance->_releasePoolStack.push_back(s_singleInstance->_curReleasePool); } return s_singleInstance; } // delete单例模式创建的对象 void PoolManager::destroyInstance() { delete s_singleInstance; s_singleInstance = nullptr; } PoolManager::PoolManager() { } // 析构函数 PoolManager::~PoolManager() { while (!_releasePoolStack.empty()) { AutoreleasePool* pool = _releasePoolStack.back(); _releasePoolStack.pop_back(); // 1、弹出自动释放池 delete pool; // 2、delete自动释放池对象 } } // 获取当前释放池 AutoreleasePool* PoolManager::getCurrentPool() const { return _curReleasePool; } void PoolManager::push(AutoreleasePool *pool) { _releasePoolStack.push_back(pool); // 1、压入释放池 _curReleasePool = pool; // 2、设为当前释放池 } // 弹出栈顶释放池,并将当前释放池指向新的栈顶释放池 void PoolManager::pop() { CC_ASSERT(_releasePoolStack.size() >= 1); _releasePoolStack.pop_back(); if (_releasePoolStack.size() > 1) { _curReleasePool = _releasePoolStack.back(); } }
貌似PoolManager功能更加简单,就是管理释放池。
四、内存管理思路
1、autorelease
new一个对象的时候,调用其autorelease将对象交给自动释放池管理
Ref* Ref::autorelease() { PoolManager::getInstance()->getCurReleasePool()->addObject(this); return this; }
2、PoolManager::getInstance()
在获取单例对象时,如果不存在则会创建一个PoolManager对象,这时候会添加两个释放池
引擎自己会维持两个默认的释放池,如果我们没有手动创建释放池,则autorelease对象都添加到栈顶默认释放池。
其实我还没弄懂这里为什么要有两个默认的释放池,一个也可以的。
3、getCurReleasePool()获取的是当前释放池,addObject()将Ref对象加入当前释放池中。
void AutoreleasePool::addObject(Ref* object) { _managedObjectArray.push_back(object); }
这样,每一个调用autorelease的Ref对象都会添加到_managedObjectArray中。
4、自动释放池的对象是怎么释放的?看AutoreleasePool:clear()函数
这里循环_managedObjectArray调用里面对象的release,调用1次release,引用计数就减1,当引用计数为0时就delete该对象。
你肯定很疑惑,在哪里会调用clear函数呢,~AutoreleasePool()会调用,但是那是在delete的时候释放的,我们看到Director类的主循环:
看到这里就明白了吧,每一次主循环都会调用clear来release自动释放池的对象,而每一帧会执行一次主循环,也就是每一帧都会清除一次。
五、手动创建释放池
我们已经知道,调用了autorelease()方法的对象将会在自动释放池池释放的时候被释放一次。虽然Cocos2d-x已经保证每一帧结束后释放一次释放池,但是如果在一帧之内生成了大量的autorelease对象,将会导致释放池性能下降。因此在生存autorelease对象密集的区域(通常是循环中)的前后,最后手动创建一个释放池。
{ AutoreleasePool pool1; // 手动创建一个释放池 for () { ref->autorelease(); // 循环里面执行autorelease,这些对象会添加到pool1中 } }
此时,引擎维护三个释放池,我们知道每一帧结束时会执行当前释放池的clear(),所以上面的那些对象就会在第一帧结束时被释放,而那些放在引擎默认释放池的autorelease对象就会在下一帧被释放,错开了释放的时间,这样就不会降低释放池的性能。
看到上面的代码,你会感到疑惑:为什么只有创建释放池,而没有释放。还记得在AutoreleasePool.h中AutoreleasePool构造函数的注释吗:不要在堆上创建,而应该在栈上。我们知道,new出来对象必须手动delete才能释放,而在栈上的变量,当作用域消失就会释放,如上面的pool1,当执行到最后一个“}”时就会调用其析构函数,看看AutoreleasePool构造和析构函数做了些什么:
创建一个AutoreleasePool是会被push到PoolManager中,而作用域系消失时就会执行析构函数,调用pop从PoolManager中删除该释放池。
这样,在这个局部作用域pool1之间所有的内存管理实际上是交给了AutoreleasePool来完成,真的是好方法。
如果上面哪里说错了,可以指出来大家讨论讨论。网上有很多关于Cocos2d-x内存管理的教程了,大家如果觉得我的很难理解的话可以查考下下面几篇精华:
1、内存管理源码分析http://cn.cocos2d-x.org/tutorial/show?id=850
2、Cocos2d-x内存管理浅说 http://www.cocoachina.com/newbie/basic/2013/0528/6290.html
3、Cocos2d-x内存管理的一种实现 http://www.cocoachina.com/applenews/devnews/2013/0531/6315.html
4、深入理解Cocos2d-x内存管理 http://www.cocoachina.com/applenews/devnews/2013/0621/6455.html
Cocos2d-x 3.1 内存管理机制