原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50317683
在使用Cocos2dx时,当我们需要某个对象实例,只要简单地调用相应静态create方法就可以得到该对象实例,使用完之后我们也无需手动地释放该对象内存而没有产生内存泄露问题。你知道其中的原理是什么么?今天我带着这个疑问,通过阅读Cocos2d-x的源码来探讨一下其内存管理的方法。下面的内容都是在Cocos2dx-3.6的基础上讲解的。
首先我们从一个程序的启动开始,一个Cocos2dx程序的程序入口再main.cpp文件中。
代码1:
int main(int argc, char *argv[])
{
AppDelegate app;
return Application::getInstance()->run();
}
在代码1中,程序启动时只做了两件事:一是创建了AppDelegate对象,二是执行Application单例的run方法。我们可以看看AppDelegate类的定义,这只是一个委托类,用来处理程序生命周期中启动、暂停等事件回调。我们进一步跟踪到run()函数内部,代码如下:
代码2:
int Application::run()
{
initGLContextAttrs();
if(!applicationDidFinishLaunching())
{
return 1;
}
long lastTime = 0L;
long curTime = 0L;
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
// Retain glview to avoid glview being released in the while loop
glview->retain();
while (!glview->windowShouldClose())
{
lastTime = getCurrentMillSecond();
director->mainLoop();
glview->pollEvents();
curTime = getCurrentMillSecond();
if (curTime - lastTime < _animationInterval)
{
usleep(static_cast<useconds_t>((_animationInterval - curTime + lastTime)*1000));
}
}
/* Only work on Desktop
* Director::mainLoop is really one frame logic
* when we want to close the window, we should call Director::end();
* then call Director::mainLoop to do release of internal resources
*/
if (glview->isOpenGLReady())
{
director->end();
director->mainLoop();
}
glview->release();
return 0;
}
在代码2中,先是执行了部分初始化工作,然后进入了游戏的主循环。到这里,我们也没有看到有关内存管理的代码,这时,我们不妨大胆猜测一下,游戏的执行说到底就是根据玩家输入(触屏)计算,然后不断刷新屏幕的过程,那它的内存管理是不是也在游戏的主循环中呢?我们进入mainLoop()函数,代码如下:
代码3:
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();
}
}
在这段代码里,我们看到Cocos2dx的注释:release the objects。这不正是释放内存的功能么!先不要激动,我们仔细看看这段代码。在主循环中,每一次循环引擎先绘制当前场景,然后调用clear()函数释放已经“死亡”的对象。这时又遇到了一个新的类PoolManager,这个类又是干什么用得呢?我们暂时不理,继续跟进到clear()函数内部。代码如下:
代码4:
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release();
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}
在代码4中,clear()函数定义在AutoreleasePool中,这段代码通过遍历一个vector,其中每个元素都是Ref类型,调用该对象的release()函数。这是我们的疑问越来越大了:前面有一个PoolManager类,现在又遇到Ref类型对象和AutoreleasePool类,这些类有什么联系吗?别急,我们先来看看release()函数。代码如下:
代码5:
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
delete this;
}
代码5中,Ref对象调用release()方法把自身的_referenceCount减1,如果该值减为0后就删除该对象。看到这里,我们是不是应该有点头绪了,referenceCount不就是引用计数器么?“引用计数”不是内存管理的方法之一么?难道Cocos2dx是采用“引用计数”法来管理内存的?带着疑问,我们把Ref类的源码看完。
代码6:
class CC_DLL Ref
{
public:
/**
* This increases the Ref's reference count.
* 引用计数加1
*/
void retain();
/**
* This decrements the Ref's reference count.
* 引用计数减1
*
* If the reference count reaches 0 after the descrement, this Ref is
* destructed.
* 如果引用计数为0,则销毁该对象
*/
void release();
/**
* Releases the ownership sometime soon automatically.
* 自动释放引用计数,返回对象本身
*/
Ref* autorelease();
/**
* Returns the Ref's current reference count.
* 返回引用计数值
*/
unsigned int getReferenceCount() const;
protected:
/**
* 构造器
*/
Ref();
public:
/**
* 析构器
*/
virtual ~Ref();
protected:
/// count of references 引用计数变量
unsigned int _referenceCount;
friend class AutoreleasePool;
};
为了便于理解,我把无关代码删除了,从Ref的头文件说明中,我们可以看到一个用于计数的的整形变量_referenceCount,它记录了当前Ref对象的引用次数。接下来,我们看看Ref是怎样实现引用计数的。
代码7:
Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
{
#if CC_ENABLE_SCRIPT_BINDING
static unsigned int uObjectCount = 0;
_luaID = 0;
_ID = ++uObjectCount;
_scriptObject = nullptr;
#endif
#if CC_REF_LEAK_DETECTION
trackRef(this);
#endif
}
Ref的构造函数中有一个初始化列表,表示_referenceCount也就是引用计数变量初始化为1。另外细心地童鞋还可能会注意到Ref类的构造器是被设置为protected访问权限的,也就是说Ref不能直接实例化,只能由其子类来实例化。
至于Ref的release()和retain()函数都比较简单,代码如下:
代码8:
void Ref::retain()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
++_referenceCount;
}
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
auto poolManager = PoolManager::getInstance();
if (!poolManager->getCurrentPool()->isClearing() && poolManager->isObjectInPools(this))
{
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
#if CC_REF_LEAK_DETECTION
untrackRef(this);
#endif
delete this;
}
}
在代码8中,retain()就是将引用计数加1,而release()函数先减少一次该对象的引用计数。当引用计数减为0时,则销毁该对象。
而autorelease()函数就有点“复杂”了,代码如下:
代码9:
Ref* Ref::autorelease()
{
PoolManager::getInstance()->getCurrentPool()->addObject(this);
return this;
}
在这里,又出现了PoolManager单例对象,autorelease()函数将当前Ref对象加入到一个pool池中,这个pool又是什么呢?在Ref的头文件说明中,有一个friend类AutoreleasePool类,是不是要将这个对象放入这个pool中呢?这个我们稍后再解释。分析到这里我们先来总结一下。
从上面的分析中,我们可以初步知道
- Cocos2dx是通过“引用计数”
- 方法来管理内存的 “引用计数”法的关键在于Ref类。该类的retain函数会将当前对象的引用计数加1,该类的release()函数会将当前对象的应用计数减1。另外,Ref对象创建后其引用计数值为1.
接下来,我们继续分析。我们前面遇到过PoolManager类和AutoreleasePool类,从命名上看,PoolManager很可能是AutoreleasePool的管理类,所以我们先看看AutoreleasePool的源码:
代码10:
class CC_DLL AutoreleasePool
{
public:
AutoreleasePool();
/**
* Create an autorelease pool with specific name. This name is useful for debugging.
* 创建一个自动释放池,并给该释放池定义一个名字
*/
AutoreleasePool(const std::string &name);
~AutoreleasePool();
/**
* Add a given object to this autorelease pool.
* 将一个对象添加到自动释放池
*/
void addObject(Ref *object);
/**
* Clear the autorelease pool.
* 清空当前释放池
*/
void clear();
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
/**
* Whether the autorelease pool is doing `clear` operation.
* 返回_isClearing,标识自动释放池是否在执行“清理”操作
*/
bool isClearing() const { return _isClearing; };
#endif
/**
* Checks whether the autorelease pool contains the specified object.
* 检查自动释放池中是否包含指定对象
*/
bool contains(Ref* object) const;
/**
* Dump the objects that are put into the autorelease pool. It is used for debugging.
* 打印调试信息
*/
void dump();
private:
/**
* The underlying array of object managed by the pool.
* 对象管理列表,存放着加入该释放池的所有对象
*/
std::vector<Ref*> _managedObjectArray;
std::string _name;
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
/**
* The flag for checking whether the pool is doing `clear` operation.
*/
bool _isClearing;
#endif
};
从源代码中得注释中可以看到,AutoreleasePool类描述了一个自动释放对象池结构。该对象中最主要的成员变量是_managedObjectArray,它是一个vector,保存了所有加入到自动释放池中得对象。
接下来,我们看看AutoreleasePool类的具体实现:
代码11:
AutoreleasePool::AutoreleasePool()
: _name("")
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}
AutoreleasePool::AutoreleasePool(const std::string &name)
: _name(name)
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}
代码11显示的是AutoreleasePool的两个构造函数,他们都只做了两件事情:一是将保存自动释放对象的_managedObjectArray的容量扩大为150(reserve是vector的成员函数,设置了vector得最小容量);二是将该自动释放池纳入PoolManager的管理中。这里我们又一次遇到PoolManager单例对象。
代码12:
void AutoreleasePool::addObject(Ref* object)
{
_managedObjectArray.push_back(object);
}
代码12中,将一个Ref对象加入到自动释放池中。到这里,我们再回头看看Ref类中得autorelease()函数,通过调用对象的autorelease()函数,会将对象加入到当前的自动释放池中去统一进行管理,并在某一时间进行释放。
再看看AutoreleasePool类中的clear()函数。代码如下:
代码13:
void AutoreleasePool::clear()
{
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = true;
#endif
std::vector<Ref*> releasings;
releasings.swap(_managedObjectArray);
for (const auto &obj : releasings)
{
obj->release();
}
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
_isClearing = false;
#endif
}
clear()函数遍历了自动释放池中得所有对象,并对每个元素调用其release()函数。前面我们已经分析过,Ref对象调用release()函数后,会将自身的引用计数值减去1,但是不一定就意味着销毁该对象,该对象是否被销毁取决于其引用计数值是否为0。
AutoReleasPool还有其它函数,都比较简单,这里就不一一分析。
接下来,我们来看看前面数次碰到的PoolManager类,代码如下:
代码14:
class CC_DLL PoolManager
{
public:
// 获取PoolManager的单例对象
static PoolManager* getInstance();
// 销毁PoolManager单例对象
static void destroyInstance();
/**
* Get current auto release pool, there is at least one auto release pool that created by engine.
* You can create your own auto release pool at demand, which will be put into auto releae pool stack.
* 获取当前使用的自动释放池
*/
AutoreleasePool *getCurrentPool() const;
bool isObjectInPools(Ref* obj) const;
friend class AutoreleasePool;
private:
PoolManager();
~PoolManager();
/**
* 将一个自动释放池加入到PoolManager中
*/
void push(AutoreleasePool *pool);
/**
* 弹出当前使用的自动释放池
*/
void pop();
static PoolManager* s_singleInstance;
// 利用一个vector保存所有的自动释放对象池
std::vector<AutoreleasePool*> _releasePoolStack;
};
故名思议,PoolManager是对所有AutoreleasePool进行管理的管理器,它是一个单例对象。它PoolManager的成员变量中存在一个vector类型的_releasePoolStack变量,用来保存所有的AutoreleasePool对象。PoolManager部分函数的具体实现如下:
代码15:
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;
}
void PoolManager::destroyInstance()
{
delete s_singleInstance;
s_singleInstance = nullptr;
}
PoolManager::PoolManager()
{
_releasePoolStack.reserve(10);
}
PoolManager的构造函数把_releasePoolStack的最小容量设置为15。然后在获取单例函数getInstance()函数中创建了一个引擎默认的自动释放池。不知道大家会不会奇怪:为什么只有new AutoreleasePool(“cocos2d autorelease pool”)而没有把新创建的自动释放子放入_releasePoolStack容器中?原因在于AutoreleasePool的构造器中已经执行了push操作,如下:
PoolManager::getInstance()->push(this);
这样当引擎启动后就会由一个默认的自动释放池被创建并加入到PoolManager得栈底(实际上是vector)。
到现在为止,我们已经查看了与内存管理相关的Ref、AutoreleasePool和PoolManager的源码,分析了它们的联系。但是我们要知道,创建一个对象后是拿来用得,如果仅仅是简单地创建一个对象(把它的引用计数值设为1)而没有使用,那么它就在下一帧马上就会被销毁,这样是没有意义的。在使用这些对象时,何时需要执行retain()操作,何时需要执行release()操作呢?这个引擎也为我们考虑好了。具体来说:在创建出一个对象后,如果调用addChild()方法把该对象添加到父节点时,会调用该对象的retain()方法;如果通过removeChild()将对象从父节点中移除时,会调用该对象的release()方法将其引用计数值减1。具体实现可以跟踪Cocos2dx的源码了解。
至此,我们就搞清楚了原来Cocos2dx使用“引用计数”和“自动释放池”的方式来管理内存,以减轻开发人员的负担。这其中涉及Ref、AutoreleasePool和PoolManager三个类。进入游戏的主循环后,引擎绘制完每一帧都会清理一次内存池,释放引用计数值为0的对象。这就是Cocos2dx的内存管理机制。