菜鸟学习Cocos2d-x 3.x——内存管理

2014-12-10 分类:Cocos2d-x / 游戏开发 阅读(394) 评论(6) 
 

亘古不变的东西

到现在,内存已经非常便宜,但是也不是可以无限大的让你去使用,特别是在移动端,那么点内存,那么多 APP要抢着用,搞不好,你占的内存太多了,系统直接干掉你的APP,所以说了,我们又要老生常谈了——内存管理。总结COM开发的时候,分析过COM的 内存管理模式;总结Lua的时候,也分析了Lua的内存回收机制;前几天,还专门写了C++中的智能指针在内存使用方面的应用;可见,内存管理无论是语言 层面,还是类库层面,都有严格的标准和实施,对于Cocos2d-x来说,也是如此。那么在Cocos2d-x中,它是如何进行内存管理的呢?这篇文章, 我就来总结一下关于Cocos2d-x的内存管理方面的知识。让你轻松度过面试官的五指关(面试Cocos2d-x时,100%会问到的问题啊,上点心 吧)。

初窥Cocos2d-x内存管理

对于探究内存管理这种比较抽象的东西,最简单的方法就是通过代码来研究,首先通过创建一个简单的场景来看看Cocos2d-x在完成创建一个对象的时候,它都干了些什么。

创建一个Scene:

auto scene = Scene::create();

函数create是一个静态函数,看看create函数的源码:

Scene *Scene::create()
{
Scene *ret = new Scene();
if (ret && ret->init())
{
ret->autorelease();
return ret;
}
else
{
CC_SAFE_DELETE(ret);
return nullptr;
}
}

现在就涉及到了Cocos2d-x的内存管理相关的知识了。在Cocos2d-x中,关于对象的创建与初始化都是使用的new和init函数搭配的方式,这种方式叫做二段式创建,由于C++中,构造函数没有返回值,无法通过构造函数确定初始化的成功与失败,所以在Cocos2d-x中就大行其道的使用了这种二段式创建的方式,用起来还不错,以后在自己的项目中,也可以采用。

由于这种方式在Cocos2d-x中经常被使用,所以触控那帮家伙就搞了个宏:CREATE_FUNC。如果想让我们的类也使用这种二段式创建的方式,只需要在我们的类中加入以下代码:

CREATE_FUNC(classname);

同时,需要定义一个init函数,这就OK了。我们来看看这个宏:

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
__TYPE__ *pRet = new __TYPE__(); \
if (pRet && pRet->init()) \
{ \
pRet->autorelease(); \
return pRet; \
} \
else \
{ \
delete pRet; \
pRet = NULL; \
return NULL; \
} \
}

话说这些东西也都是基础的C++知识,没有多少需要说的了,当你看到代码中的ret->autorelease(),一脸茫然,是的,你已经看到了Cocos2d-x的内存管理的触角了。

ret->autorelease()是什么?当我使用create函数创建了场景以后,我并没有去delete,这也没有问题。问题就发生在这个autorelease的使用上。序幕说完了,让我们真正的开始Cocos2d-x的内存管理吧。

在Cocos2d-x中,关于内存管理的类有:

  • Ref类;
  • AutoreleasePool类;
  • PoolManager类。

Ref类几乎是Cocos2d-x中所有类的父类,它是Cocos2d-x中内存管理的最重要的一环;上面说的autorelease函数就Ref类的成员函数,Cocos2d-x中所有继承自Ref的类,都可以使用Cocos2d-x的内存管理。

AutoreleasePool类用来管理自动释放对象。

PoolManager用来管理所有的AutoreleasePool,这个类是使用单例模式实现的。

下面就通过对上述三个类的源码进行分析,看看Cocos2d-x到底是如何进行内存管理的。

Ref类

先来看看Ref类的定义,以下是Ref类的头文件定义:

class CC_DLL Ref
{
public:
/**
* 获取对象的所有权
* 增加对象的引用计数
*/
void retain(); /**
* 立即释放对象的所有权
* 同时会减少对象的引用计数,当引用计数达到0时,直接销毁这个对象
*/
void release(); /**
* 自动释放对象的所有权
* 将对象添加到自动释放池
* 当在下一帧开始前,当前的自动释放池会被回收掉,并且对自动释放池中的所有对象
* 执行一次release操作,当对象的引用计数为0时,对象会被释放掉。
*/
Ref* autorelease(); /**
* 获得对象的当前引用计数
* 当创建对象的时候,引用计数为1
*/
unsigned int getReferenceCount() const;
};

对于release函数的实现,这里需要特别总结一下,先看看它的实现:

void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should 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))
{
// 这里是非常重要的一点,在我们使用Cocos2d-x中经常出错的地方
// 当引用计数为0,同时这个对象还存在于autorelease池中的时候,就会出现一个断言错误
// 可以想到,当这个对象引用计数为0时,就表示需要释放掉,如果它还在autorelease池中,
// 当在autorelease池中再次被释放时,就会出现错误,这种错误是不了解Cocos2d-x内存管理的
// 编程人员经常犯的错误。
//
// 出现这个错误的原因在于new/retain和autorelease/release没有对应使用引起的
CCASSERT(false, "The reference shouldn't be 0 because it is still in autorelease pool.");
}
#endif
delete this;
}
}

上面也说道了,对于new和autorelease需要匹配使用,retain和release也需 要匹配使用,否则就会出现断言错误,或者内存泄露;在非Debug模式下,就可能直接闪退了。这就是为什么我们在使用create函数的时候,new成功 以后,就顺便调用了autorelease,将该对象放入到自动释放池中;而当我们再次想获取该对象并使用该对象的时候,需要使用retain再次获得该 对象的所有权,当然了,在使用完成以后,你应该记得调用release去手动完成释放工作,这是你的任务。例如以下代码:

auto obj = Scene::create();
obj->autorelease(); // Error

这是错误的,在create中,在创建成功的情况下,已经将obj对象放到了autorelease pool中了;当你再次放入autorelease pool后,当销毁autorelease pool以后,就会出现两次销毁一个对象的情况,出现程序的crash。再例如以下代码也是错误的:

auto obj = Scene::create();
obj->release(); // Error

当使用create函数创建对象以后,obj没有所有权,当再次调用release时,就会出现错误的对象释放。而正确的做法应该如下:

auto obj = Scene::create(); // 这里retain和release对应,release一个已经被autorelease过的对象(例如通过create函数构造的对象)必须先retain
obj->retain();
obj->release();

这引用计数,又让我想起了COM中的AddRef和Release。

AutoreleasePool类

AutoreleasePool类是Ref类的友元类,先来看看Autorelease类的声明。

class CC_DLL AutoreleasePool
{
public:
/**
* 不能在堆上创建AutoreleasePool对象,只能在栈上创建
* 这就决定过了,当出了对应的作用域,AutoreleasePool对象就会被自动释放,例如RAII技巧实现的
*/
AutoreleasePool(); /**
* 创建一个带有指定名字的autorelease pool对象
* 对于调试来说,这个名字是非常有用的。
*/
AutoreleasePool(const std::string &name); ~AutoreleasePool(); /**
* 向autorelease pool中添加一个ref对象
* 同一个对象可以多次加入同一个自动释放池中(貌似会触发断言错误)
* 当自动释放池被销毁的时候,它会依次调用自动释放池中对象的release()函数
*/
void addObject(Ref *object); /**
* 清理自动释放池
* 依次调用自动释放池中对象的release()函数
*/
void clear(); #if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
/**
* 判断当前是否正在执行自动释放池的清理操作
*/
bool isClearing() const { return _isClearing; };
#endif /**
* 判断自动释放池是否包含指定的Ref对象
*/
bool contains(Ref* object) const; /**
* 打印autorelease pool中所有的对象
*/
void dump(); private:
/**
* 所有的对象都是使用的std::vector来存放的
*/
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类来说,它的实现很简单,就是将简单的将对象保存在一个std::vector中,在释放这个AutoreleasePool的时候,对保存在std::vector中的对象依次调用对应的release函数,从而完成对象的自动释放。

PoolManager类

这货又是干什么的?当我们在阅读AutoreleasePool的源码的时候,在它的构造函数中,你会发现如下代码:

AutoreleasePool::AutoreleasePool()
: _name("")
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
, _isClearing(false)
#endif
{
_managedObjectArray.reserve(150);
PoolManager::getInstance()->push(this);
}

在AutoreleasePool的析构函数中,又有如下代码:

AutoreleasePool::~AutoreleasePool()
{
CCLOGINFO("deallocing AutoreleasePool: %p", this);
clear(); PoolManager::getInstance()->pop();
}

哦,原来,我们把AutoreleasePool对象又放到了PoolManager里了;原 来,PoolManager类就是用来管理所有的AutoreleasePool的类,也是使用的单例模式来实现的。该PoolManger有一个存放 AutoreleasePool对象指针的stack,该stack是由std::vector实现的。需要注意的是,cocos2d-x的单例类都不是 线程安全的,跟内存管理紧密相关的PoolManager类也不例外,因此在多线程中使用cocos2d-x的接口需要特别注意内存管理的问题。关于更安 全的单例模式,感兴趣的同学可以去阅读这篇《C++设计模式——单例模式》。接下来,我们先看看PoolManager的头文件定义。

class CC_DLL PoolManager
{
public:
/**
* 获得单例
*/
static PoolManager* getInstance(); /**
* 销毁单例
*/
static void destroyInstance(); /**
* 获得当前的autorelease pool,在引擎中,至少会有一个autorelease pool
* 在需要的时候,我们可以创建我们自己的release pool,然后将这个autorelease pool添加到PoolManager中
*/
AutoreleasePool *getCurrentPool() const; /**
* 判断指定的对象是否在其中的一个autorelease pool中
*/
bool isObjectInPools(Ref* obj) const; friend class AutoreleasePool; private:
PoolManager();
~PoolManager(); /**
* 将AutoreleasePool对象添加到PoolManager中
*/
void push(AutoreleasePool *pool); /**
* 从PoolManager中移除AutoreleasePool对象
*/
void pop(); static PoolManager* s_singleInstance; /**
* 用来保存所有的AutoreleasePool对象
*/
std::deque<AutoreleasePool*> _releasePoolStack;
AutoreleasePool *_curReleasePool;
};

关于PoolManager中各个函数的实现也是非常简单的,这里不做累述,各位可以去阅读Cocos2d-x的源码。

问题来了

说了这么多,代码也列了这么多,我们create一个对象以后,放到了 AutoreleasePool中去了,最终,在调用AutoreleasePool的clear函数的时候,会对AutoreleasePool管理的 所有对象依次调用release操作。啊哈!貌似哪里不对,我一直都没有说最终谁会调用这个clear函数啊?是的。看下面这段在导演类中的代码,我想你 会明白的。

void DisplayLinkDirector::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (! _invalid)
{
drawScene(); // release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}

上面的代码说明的事实是:在图像渲染的主循环中,如果当前的图形对象是在当前帧,则调用显示函数,并 调用AutoreleasePool::clear()减少这些对象的引用计数。mainLoop是每一帧都会自动调用的,所以下一帧时这些对象都被当前 的AutoreleasePool对象release了一次。这也是AutoreleasePool「自动」的来由。

总结

好了,总结的差不多了,对于Cocos2d-x中的内存管理总结的差不多了。对于Cocos2d-x 中的内存管理,我个人认为,请时刻关注着这个对象的引用计数,retain和release,new和autorelease需要匹配使用,防止不必要的 错误发生。总结了这么多,还是那句话。

纸上得来终觉浅,绝知此事要躬行。

只有经过实际的使用,在经过代码的洗练,才能更好的去掌握这些。在Cocos2d-x中,很多地方已 经进行了autorelease,或者retain了,我们就不必再次进行这些操作,比如create,再比如在调用addChild方法添加子节点时, 自动调用了retain。对应的通过removeChild,移除子节点时,自动调用了release。这些地方稍微不注意,就可能会让你掉入“坑”中。 努力吧,伙计们。

2014年12月10日 于深圳。

菜鸟学习Cocos2d-x 3.x——内存管理的更多相关文章

  1. 【Spark-core学习之八】 SparkShuffle & Spark内存管理

    [Spark-core学习之八] SparkShuffle & Spark内存管理环境 虚拟机:VMware 10 Linux版本:CentOS-6.5-x86_64 客户端:Xshell4 ...

  2. iOS 阶段学习第九天笔记(内存管理)

    iOS学习(C语言)知识点整理 一.内存管理 1)malloc , 用于申请内存; 结构void *malloc(size_t),需要引用头文件<stdlib.h>:在堆里面申请内存,si ...

  3. iOS学习08之C语言内存管理

    本次主要学习和理解C语言中的内存管理 1.存储区划分 按照地址从高到低的顺序:栈区,堆区,静态区,常量区,代码区 1> 栈区:局部变量的存储区域 局部变量基本都在函数.循环.分支中定义 栈区的内 ...

  4. Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

    很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确 ...

  5. 分布式缓存技术memcached学习(三)——memcached内存管理机制

    几个重要概念 Slab memcached通过slab机制进行内存的分配和回收,slab是一个内存块,它是memcached一次申请内存的最小单位,.在启动memcached的时候一般会使用参数-m指 ...

  6. 《objective-c基础教程》学习笔记(十)—— 内存管理

    本篇博文,将给大家介绍下再Objective-C中如何使用内存管理.一个程序运行的时候,如果不及时的释放没有用的空间内存.那么,程序会越来越臃肿,内存占用量会不断升高.我们在使用的时候,就会感觉很卡, ...

  7. libevent源码学习(2):内存管理

    目录 内存管理函数 函数声明 event-config.h 函数定义 event_mm_malloc_ event_mm_calloc_ event_mm_strdup_ event_mm_reall ...

  8. JVM学习笔记(三)------内存管理和垃圾回收【转】

    转自:http://blog.csdn.net/cutesource/article/details/5906705 版权声明:本文为博主原创文章,未经博主允许不得转载. JVM内存组成结构 JVM栈 ...

  9. JVM学习笔记(三)------内存管理和垃圾回收

    JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生 ...

  10. OC学习10——内存管理

    1.对于面向对象的语言,程序需要不断地创建对象.这些对象都是保存在堆内存中,而我们的指针变量中保存的是这些对象在堆内存中的地址,当该对象使用结束之后,指针变量指向其他对象或者指向nil时,这个对象将称 ...

随机推荐

  1. Android真机调测Profiler

    U3D中的Profile也是可以直接在链接安卓设备运行游戏下查看的,导出真机链接U3D的Profile看数据,这样能更好的测试具体原因. 大概看了下官方的做法,看了几张帖子顺带把做法记录下来. 参考: ...

  2. [Xcode 实际操作]一、博主领进门-(4)设置项目的属性

    目录:[Swift]Xcode实际操作 本文将演示如何设置项目的属性. 点击项目名称[DemoApp],打开项目信息面板. [Identity识别]设置区域 [Display Name]:DemoAp ...

  3. SpringBoot2.0 整合 Swagger2 ,构建接口管理界面

    一.Swagger2简介 1.Swagger2优点 整合到Spring Boot中,构建强大RESTful API文档.省去接口文档管理工作,修改代码,自动更新,Swagger2也提供了强大的页面测试 ...

  4. hyperledger fabric 1.0.5 分布式部署 (六)

    如何在相同的peer 节点上创建多个 channel 作者在hyperledger fabric 1.0.5 分布式部署 (五)已经向读者们介绍了一个简单的fabric 的部署流程,那么根据上一篇博客 ...

  5. day04 基本类型包装类

  6. Codeforces Round #497 (Div. 2)B. Turn the Rectangles

    Bryce1010模板 http://codeforces.com/contest/1008/problems #include <bits/stdc++.h> using namespa ...

  7. 洛谷 P1121 环状最大两段子段和

    https://www.luogu.org/problemnew/show/P1121 不会做啊... 看题解讲的: 答案的两段可能有两种情况:一是同时包含第1和第n个,2是不同时包含第1和第n个 对 ...

  8. 081 Search in Rotated Sorted Array II 搜索旋转排序数组 ||

    这是 “搜索旋转排序数组”问题的跟进:如果数组元素允许重复,怎么办?这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?假设按照升序排序的数组在预先未知的某个关键点上旋转.(例如, 0 1 2 4 ...

  9. mirror.js 整合redux的好工具

    mirror.js 很简单,让state管理更方便了,没有新增api,值 得使用 https://github.com/yurizhang/mirror package.json { "na ...

  10. office 导出问题

    就用程序池右击项目高级设置 应用程序池的项目中的标识改为 LocalSystem 启用32位应用程序设为true或false