cocos2dx源码分析之二:引擎的内存管理
cocos2dx引擎有着自己的一套内存管理系统,这套系统是基于引用计数的,什么是引用计数,简单理解就是用一个变量时刻记录有多少地方持有着该实例,当计数为0时,代表该对象可以被销毁回收内存了。首先来看最关键的基类Ref.
class CC_DLL Ref { public: void retain(); void release(); Ref* autorelease(); unsigned int getReferenceCount() const; protected: Ref(); public: virtual ~Ref(); protected: unsigned int _referenceCount; friend class AutoreleasePool; };
这里删掉了一些与理解内存管理无关的代码,整体上看Ref类很简洁。_referenceCount就是用于记录引用计数的变量
Ref::Ref() : _referenceCount(1) }
从构造函数可以看出,对象每次实例化_referenceCount的初始值是1.
void Ref::retain() { CCASSERT(_referenceCount > 0, "reference count should be greater than 0"); ++_referenceCount; }
成员函数retain用于增加引用计数。
void Ref::release() { CCASSERT(_referenceCount > 0, "reference count should be greater than 0"); --_referenceCount; if (_referenceCount == 0) { delete this; } }
成员函数release用于减小引用计数,当引用计数为0时,释放该实例。
Ref* Ref::autorelease() { PoolManager::getInstance()->getCurrentPool()->addObject(this); return this; }
autorelease方法会将当前实例添加到一个自动释放池里,我们来看一下PoolManager.
class CC_DLL PoolManager { public: static PoolManager* getInstance(); static void destroyInstance(); AutoreleasePool *getCurrentPool() const; bool isObjectInPools(Ref* obj) const; friend class AutoreleasePool; private: PoolManager(); ~PoolManager(); void push(AutoreleasePool *pool); void pop(); static PoolManager* s_singleInstance; std::vector<AutoreleasePool*> _releasePoolStack; };
依然删除了一些对于理解内存管理无用的代码,PoolManager使用单例设计模式,全局只有一个实例来统一管理所有的自动释放池,可以看到
_releasePoolStack是一个存放自动释放池的栈,PoolManager通过成员函数push和pop来管理(入栈出栈)这个栈,我们来看一下PoolManager的具体使用
PoolManager* PoolManager::getInstance() { if (s_singleInstance == nullptr) { s_singleInstance = new (std::nothrow) PoolManager(); // Add the first auto release pool new AutoreleasePool("cocos2d autorelease pool"); } return s_singleInstance; }
可以看到新建PoolManager时,也会创建一个AutoreleasePool实例,但是并没有显式加入到栈中,那我们就来看下AutoreleasePool这个类。
class CC_DLL AutoreleasePool { public: AutoreleasePool(); AutoreleasePool(const std::string &name); ~AutoreleasePool(); void addObject(Ref *object); void clear(); bool contains(Ref* object) const; void dump(); private: std::vector<Ref*> _managedObjectArray; std::string _name; };
依然删除了一些代码,从头文件的这些成员变量和成员函数不难看出AutoreleasePool就是实际控制每个实例内存管理的容器,_managedObjectArray存放所有需要自动管理的实例,addObject可以把实例添加到数组,contains用于判断某个实例是否已经在数组中,clear用于清空数组。我们先来看构造函数
AutoreleasePool::AutoreleasePool(const std::string &name) : _name(name) { _managedObjectArray.reserve(150); PoolManager::getInstance()->push(this); }
可以看到构造函数需要一个name来标记这个自动释放池,然后自动释放池会被加入到PoolManager中,这就解释了上面为啥只是创建了自动释放池,却没有显式的加入到PoolManager中。
void AutoreleasePool::clear() { std::vector<Ref*> releasings; releasings.swap(_managedObjectArray); for (const auto &obj : releasings) { obj->release(); } }
可以看到clear函数中遍历数组,依次调用所有实例的release方法将实例的引用计数减1.那么对象是何时被加入到自动释放池的呢?
#define CREATE_FUNC(__TYPE__) \ static __TYPE__* create() \ { \ __TYPE__ *pRet = new(std::nothrow) __TYPE__(); \ if (pRet && pRet->init()) \ { \ pRet->autorelease(); \ return pRet; \ } \ else \ { \ delete pRet; \ pRet = nullptr; \ return nullptr; \ } \ }
可以看到这个CREATE_FUNC宏定义new了一个实例之后,调用了对象的init方法,然后调用autorelease方法,这个方法就会将对象加入到自动释放池。那么自动释放池什么时候调用clear来释放对象呢?
在源码分析的第一篇中有讨论,游戏主循环主要就做了两件事,第一件事就是绘制,第二件事就是清空自动释放池。
void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (_restartDirectorInNextLoop) { _restartDirectorInNextLoop = false; restartDirector(); } else if (! _invalid) { drawScene(); // release the objects PoolManager::getInstance()->getCurrentPool()->clear(); } }
到这里内存管理的部分基本就分析完了,但是有两个地方还要特别说明一下:
第一,仔细观察PoolManager可以发现,引擎是可以支持多个自动释放池的,PoolManager初始化时同时会实例化一个自动释放池,压入到栈中,用户还可以自己创建自动释放池压入栈中
AutoreleasePool* PoolManager::getCurrentPool() const { return _releasePoolStack.back(); }
但是,主循环释放的只是栈顶的自动释放池,而不去管其他的,这么设计可能的原因是,多数情况下一个自动释放池就满足了设计需求,但是当有特殊要求,比如大量的实例存在的时间不足一帧,这时我们可以自己创建一个自动释放池,压入栈中,之后加入到自动释放池的实例实际上都是加入到了自己创建的自动释放池中,然后clear,出栈自己创建自动释放池,这样并不影响其他实例的内存管理。
第二,很多新手都会很疑惑,我新创建的一个实例,比如说一个精灵,初始引用计数是1,然后我加入到节点树中,比如addChild到一个Layer上,这时引用计数是2,第一帧结束时会release,引用计数变成了1,下一帧再release,引用计数不就变成0了就会被释放了吗?关于这个问题我们再仔细看clear方法
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
clear方法中新创建了一个数组,然后调用了swap方法,而这里的swap方法会使得_managedObjectArray被清空,也就是说加入到自动释放池的实例,都只会被自动release一次。其实,这个内存管理机制主要就是避免那些创建了但是却没有被加入到场景中的节点,从而造成内存溢出。很多情况下还是需要我们自己去管理内存的。