热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨

在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。

Cocos2d-x高级开发教程读书笔记

Boost库引入的智能指针

内存管理机制:第2章

 

就比如一直计算鱼运动过程中的角度,可以写一个函数。
通过CallFunc与CCRepeatForever实现与写在update函数中一样的效果。。

P79

动态类的结构与动作原理

 

CCPlatformMacros.h与属于有关的宏定义

第9章

地图 纹理编辑器:Tiled

碎图合成工具:TexturePacker

11章

学习笔记:

第1章

AppDelegate.h和AppDelegate.cpp文件是cocos2d-x游戏的通用入口文件,

FPS:每秒帧率,也就是屏幕每秒重绘的次数

绘制间隔:两次绘制的时间间隔,因为这个间隔的倒数就是FPS的上限

第2章

场景:内容相对不变的游戏元素集合合称为场景 CCScene

流程控制:游戏在场景之间切换的过程

层:属于场景之下的游戏元素,一个复杂场景会可以拥有多个层,一个层会显示一部分的视觉元素,CCLayer

精灵:层和场景是其他游戏元素的容器,属于层,是场景中出现的可见图形,如果不向层和场景中添加可见的游戏元素,它们看起来就是一直是透明的。CCSprite

根据游戏中的层次关系和隶属关系,可以生成一个树型关系图来。

关系图中的每一个节点称作为节点(Node),关系图则称为渲染树(rendering tree),

渲染场景的过程就是遍历渲染树的过程。每个节点都有一定的子节点。如果要渲染一个节点,

必须先渲染他的所有子节点。

任何可见的游戏元素都派自Cocos2d-x节点(CCNode)

常见的游戏元素有场景,层,精灵等,

层和精灵都是普通的节点,因此,即使向精灵中添加精灵,向场景中添加精灵,向精灵中添加层,这些操作也都没有禁止。必要时可以尝试各种组织层次。

动作:作用于游戏元素,分为持续性与瞬时动作,CCAction,由CCAction类派生出持续性动作类CCACtionInterval和瞬时动作类CCActionInstant,所有的动作都派生于以上两类之一

动画:是一种特殊的持续性动作,只能应用于精灵上,用于实现帧动画效果。

静止的图片叫做帧,帧的序列代表一个动画(定时更换图片)。

动画帧序列:CCAnimation

动画:CCAnimate

Cocos2d-x拥有一个包含其它所有头文件的文件”cocos2d.h”,通常,我们只需要在使用时包含这个头文件,就可以使用引擎的全部功能了。扩展功能不算。

类的命名:类库+类名称,采用驼峰法法则,cocos2d的缩写是CC,因此Cocos2d-x的类都拥有CC前缀

Cocos2d-x不使用传统的值类型,所有的对象都创建在堆上(个别除外),然后通过指针引用,不过不直接使用构造函数初始化成员值,创建Cocos2d-x中的类对象通常有两种方法

1、首先使用new操作符创建一个未初始化的对象,然后调用init系列方法来初始化

2、使用静态的工厂方法直接创建一个对象

工厂方法:类提供的静态函数。只要提供必要的参数,就会返回一个完成了初始化的对象。一般以create开头

区别:方法1创建的对象的所有权已经属于调用者了,使用工厂方法的所有权并不属于调用者(引用计数是否加1了),因此使用构造函数创建的对象需要调用者负责释放,而使用工厂方法的对象则不需要。

在游戏中,我们需要不断的创建新的游戏元素,通常采取的方法:从Cocos2d-x提供的游戏元素类派生出新的类,并在初始化方法(init  虚函数,且调用基类的init函数,进行初始化从基类中继承的成员)中建立我们所需的游戏元素。

选择器:函数指针机制。

Cocos2d-x,提供了一系列关于Object-c中创建选择器语法的宏,用来创建函数指针,这些宏都只有一个参数Selector(如:HelloWorld::menuCloseCallback ),表示指向的

类方法。

常用的宏

l schedule_selector(SELECTOR) 

l callfunc_selector(SELECTOR) 

l callfuncN_selector(SELECTOR) 

l callfuncND_selector(SELECTOR) 

l callfunc_selector(SELECTOR) 

l menu_selector(SELECTOR) 

l event_selector(SELECTOR) 

l compare_selector(SELECTOR) 

属性:C++的类成员只有方法与字段,没有属性和事件,为了实现Object-c中提供的属性

功能,我们不得不使用方法模拟get和set访问器。Cocos2d-x规定了属性访问器的方法名称以get或set为前缀,后接属性名(其它就是成员变量)。

如:在类中编写

 

int tag; 

l int getTag() { return tag; } 

l void setTag(int aTag) { tag = aTag; } 

为每一个属性编写一个set get 是一个很枯燥的事情。 为了避免重复性的工作,Cocos2d-x提供了一系列宏来帮助我们方便地创建属性。表2-1列举了所有属性相关的宏,它们定义在引擎目录中的"platform/CCPlatformMacros.h"中。 

n 表2-1 Cocos2d-x中与属性相关的宏 

 

 

l 宏 

 

 

l 描述 

 

 

l CC_PROPERTY 

 

 

l 定义一个属性及其访问器,没有实现。 

l 通常用于简单的值类型 

 

 

l CC_PROPERTY_READONLY 

 

 

l 定义一个属性,只包含get访问器,没有实现 

 

 

l CC_PROPERTY_PASS_BY_REF 

 

 

l 定义一个属性,访问器使用引用类型传 

l 递参数,没有实现。通常用于结构体类型 

 

 

l CC_PROPERTY_READONLY_PASS_BY_REF 

 

 

l 定义一个属性,只包含get访问器, 

l 且使用引用类型传递参数,没有实现 

 

 

 

 

(续) 

 

l 宏 

 

 

l 描述 

 

 

l CC_PROPERTY_READONLY_PASS_BY_REF 

 

 

l 定义一个属性,只包含get访问器, 

l 且使用引用类型传递参数,没有实现 

 

 

l CC_SYNTHESIZE 

 

 

l 同CC_PROPERTY,实现了访问器方法 

 

 

l CC_SYNTHESIZE_READONLY 

 

 

l 同CC_PROPERTY_READONLY,实现了访问器方法 

 

 

l CC_SYNTHESIZE_PASS_BY_REF 

 

 

l 同CC_PROPERTY_PASS_BY_REF, 

l 实现了访问器方法 

 

 

l CC_SYNTHESIZE_READONLY_PASS_BY_REF 

 

 

l 同CC_PROPERTY_READONLY_PASS_BY_REF, 

l 实现了访问器方法 

 

 

l CC_SYNTHESIZE_RETAIN 

 

 

l 同CC_PROPERTY,实现了访问器方法。 

l 用于派生自CCObject的类型,访问器采取 

l Cocos2d-x的内存管理机制自动维护 

l 对象的引用计数 

 

 

这些宏只要写在类的定义之中即可。每个宏都有3个参数,分别是:varType,属性类型,如果属性类型是对象,需要写成指针的形式;varName,属性的私有字段名称;funName,属性的访问器名称,也就是紧接在get或set前缀后的部分。 

利用Cocos2d-x提供的宏,上面的Tag属性定义就可以用下面一条语句代替了: 

CC_SYNTHESIZE(int, tag, Tag) 

 

单例:单例模式保证了全局有且只有一个实例对象,保证自动地初始化对象,使得程序在任何时候任何地方都可以访问、获取该对象。比如:

CCDirector::sharedDirector()通过这个函数,只有在第一次调用的时候才会生成一个实例对象,后续还调用这个函数,则会返回前一个对象的引用。

 

C++中的Cocos2d-x内存管理:

现有的实现智能管理内存的技术:引用计数与垃圾回收

引用计数:它是一种很有效的机制,通过对每个对象一个引用计算器,记录该对象

当前被引用的次数。当引用计数为0时,标志着该对象的生命周期结束。自动触发对象的回收释放。

引用计数的重要原则:每一个程序片段必须负责任地维护引用计数,在需要维持对象生存的程序段的开始和结束分别增加和减少一次引用计数,这样我们就可以实现十分灵活的智能内存管理了。引用计数问题解决了对象的生命周期管理问题,但堆碎片化和管理烦琐的问题仍然存在。

垃圾回收:它通过引入一种自动的内存回收器,试图将程序员从复杂的内存管理任务中完全解放出来。它会自动跟踪每一个对象的所有引用,以便找到所有正在使用的对象,然后释放其余不再需要的对象。垃圾回收器还可以压缩使用中的内存,以缩小堆所需要的工作空间。垃圾回收可以防止内存泄露,有效地使用可用内存。但是,垃圾回收器通常是作为一个单独的低级别的线程运行的,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用过的对象进行清除和回收,程序员不能手动指派垃圾回收器回收某个对象。回收机制包括分代复制垃圾回收、标记垃圾回收和增量垃圾回收。 

注:这里面的引用应该说的是多了一个指针指向。

在谈论Cocos2d-x代码风格的时候,多次提到Cocos2d-x来源于Cocos2d-iPhone,因此为了与Objective-C一致

Cocos2d-x也用于了引用计数与自动回收的内存管理机制。

为了实现对象的引用计数记录,Cocos2d-x实现了自己的根类CCObject,引擎中的所有类都派生自CCObject类

每个对象包含一个用来控制生命周期的引用计数器,它就是CCObject的成员变量

m_uReference,我们可以通过retainCount()方法获得对象当前的引用计数值。

在对象通过构造函数创建时,该引用值赋值为1,表示对象由创建者所引用,在其他

地方需要引用对象时,我们会调用retain( )方法,使其引用计数増1,表示获取该对象

的引用权;在引用结束的时候调用release()方法,使其引用计数值减1,表示释放对象

的引用权。

另一个很有趣的方法是autorelease(),其作用是将对象放入自动回收池

(CCAutoleasePool),当回收池自身被释放的时候,它就会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。回收池可以手动创建和释放。除此之外,引擎在每次游戏循环开始之前(一个帧属于一次循环)也会创建一个回收池,在循环结束后释放回收池。因此,即使我们没有手工创建和释放回收池,每一帧结束的时候,自动回收池中的对象也都会被执行一次release()方法。

注:比如:对象创建后,引用计数为1 ,执行一次retain()后,引用计数为2,执行一次release()后,引用计数回到1,执行一次autorelease()后,对象的引用计数值并没有立即减1,但是在下一帧开始前,对象会被释放掉。(游戏一直在重复循环绘制,这样就形成了游戏动态的效果),。

可以使用控制台显示的日志观察:

CCLog

调用autorelease()方法的对象,将会在自动回收池释放的释放被释放一次,虽然,cocos2d-x已经保证了每一帧结束后释放一次回收池,并在下一帧开始前创建一个新的回收池,但是我们也应该考虑到回收池本身维护着一个将要执行释放操作的对象列表,如果在一帧之内产生了大量的autorelease对象,将会导致回收池性能下降。因此,在生成autorelease对象密集的区域(通常在循环中)的前后,最好手动创建并释放一个回收池,这样可以提前释放一次。

通过回收池管理器

CCPoolManager的push()和pop(),来实现创建和释放回收池,

如:

CCPoolManager::sharedPoolManager()->push(); 

l for(int i=0; i

l { 

l CCString* dataItem = CCString::createWithFormat("%d", Data[i]); 

l stringArray->addObject(dataItem); 

l } 

l CCPoolManager::sharedPoolManager()->pop(); 

 

不难看出,自动回收池是可嵌套的。通常,引擎维护着一个回收池,所有的autorelease对象都添加到了这个池中。多个自动回收池排列成栈结构,当我们手动创建了回收池后,回收池会压入栈的顶端,autorelease对象仅添加到顶端的池中。当顶层的回收池被弹出释放时,它内部所有的对象都会被释放一次,此后出现的autorelease对象则会添加到下一个池中。 

n 在自动回收池嵌套的情况下,每一个对象是如何加入自动回收池以及如何释放的,相关代码如下所示: 

 

l //步骤a 

l obj1->autorelease(); 

l obj2->autorelease(); 

l //步骤b 

l CCPoolManager::sharedPoolManager()->push(); l

l //步骤c 

l for(int i=0; i

l obj_array[i]->autorelease(); 

l } l

l //步骤d 

l CCPoolManager::sharedPoolManager()->pop(); l

l //步骤e 

l obj3->autorelease(); 

 

 n 上述代码的具体过程如下图所示,当执行完步骤a时,obj1与obj2被加入到回收池1中,如图2-7a所示;步骤b创建了一个新的回收池,此时回收池2接管所有autorelease操作,如图2-7b所示;步骤c是一个循环,其中把n个对象加入回收池2中,如图2-7c所示;步骤d释放了回收池2,因此回收池2中的n个对象都被释放了一次,同时回收池1接管autorelease操作;步骤e调用obj3的autorelease()方法,把obj3加入回收池1中,

 

 

 


 

 

注:这里说的释放,不是释放内存,而是对引用计数减1.,当引用计数为0时,自动释放内存

工厂方法:

 

工厂方法是程序设计中一个经典的设计模式,指的是基类中只定义创建对象的接口,将实际的实现推迟到子类中。在这里,我们将它稍加推广,泛指一切生成并返回一个对象的静态函数。一个经典的工厂方法如同这样: 

l CCObject* factoryMethod() { 

l CCObject* ret = new CCObject(); 

l //在这里对ret对象进行必要的初始化操作

l return ret; 

l }

 

这段看起来正常的代码其实隐藏着一个问题:工厂方法对ret对象的引用在函数返回时已经结束(这里的意思是,返回一个对象指针,按照原则的话,调用这个函数的时候引用次数加1,这样retaincount就变成了2,因为使用new的时候就已经为1了,但是new出来的那个对象指针,出了这个函数,我们不可能再使用它,所以说我们应该让这个对象的引用计数减1),但是它没有释放对ret的引用,埋下了内存泄露的隐患。但是,如果在函数返回前就执行release(),这显然是不合适的,因为这会触发对象的回收,再返回的对象指针就成为了错误指针。 

n autorelease()方法很好地解决了这个问题。此函数结束时我们已经丧失了对ret的引用,为了把ret对象传递给接受者,需要对它进行一次autorelease操作,这是因为虽然我们调用了autorelease方法,但是对象直到自动回收池释放之前是不会被真正释放掉的(通常Cocos2d-x会在每一帧之间释放一次自动回收池),调用者有足够的时间来对它进行retain操作以便接管ret对象的引用权(这就是我们说的规则,调用者对引用计数加1 )。因此,Cocos2d-x的执行机制很巧妙地保证了回收池中的对象不会在使用完毕前释放。利用autorelease()修改后的工厂方法如下: 

CCObject* factoryMethod() { 

l CCObject* ret = new CCObject(); 

l //这里对ret对象进行必要的初始化操作

l ret->autorelease(); 

l return ret; l }n 

我们曾提到两种创建对象的方式。

使用构造函数创建对象时,对象的引用计数为1,因此调用者需要在使用完毕后谨慎地释

放对象(就像我们刚刚的函数中的new 出来的对象指针,这是使用构造函数的,在出函数后就不再使用了,我们必须让这个对象的引用次数释放一次。如果不使用autorelease(),程序必须脑子里记住上一次有一个不再使用的指针对象,在出了它的范围后要马上使用release(),如果不马上使用就得一直记住,最后肯定还是要加上这个release());

使用工厂方法创建对象时,虽然引用计数也为1,但是由于对象已经被放入了自动回收池,因此调用者没有对该对象的引用权,除非我们人为地调用了retain()来获取引用权,否则,

不需要主动释放对象(释放说的是release()进行一次引用计数减1)。 因为会在这一帧结束的释放一次。

注:真正的内存释放只有这种情况下才会发生

1、当引用计数为0时,自动调用析构函数释放内存

关于对象传值

将一个实例对象赋值给某一指针作为引用的时候,为了遵循内存管理的原则,如果前一个指针对象不再使用了,我们需要获得新对象的引用权,释放旧对象的引用权,此时,release()

和retain()的顺序特别重要,比如:

void SomeClass::setObject(CCObject* other) { 

l this->object->release(); 

l other->retain(); 

l this->object = other; 

l }n

 这里存在的隐患是,当other和object实际上指向同一个对象时,第一个release()可能会触发该对象的回收,这显然不是我们想看到的局面,所以应该先执行retain()来保证other对象有效,然后再释放旧对象: 

l void SomeClass::setObject(CCObject* other) { 

l other->retain(); 

l this->object->release(); 

l this->object = other; 

l }

其他可行的解决方案也有很多,例如使用autorelease()方法来代替release()方法,或在赋值前判断两个对象是否相同。在Google的Objective-C编程规范中,推荐使用autorelease()方法代替release()方法。 

 释放:release()还是autorelease()? 

 上面的两个例子实际上提出了一个问题:在使用autorelease()可以达到与release()同样的效果,甚至还能避免release()的许多隐患的情况下,是不是应该完全用autorelease()代替release()呢? 

n 实际上,autorelease()并不是毫无代价的,其背后的垃圾池机制同样需要占用内存和CPU资源,每次执行autorelease()的过程,实际上对应的是执行成对的retain()和release(),以及一次成对的容器存取,还包括其他的逻辑判断。过多不必要的autorelease()将导致垃圾池臃肿膨胀,在存在大量内存操作的程序中会尤为严重地挤占本来就紧张的系统资源。 

n 此外,autorelease()只有在自动释放池被释放时才会进行一次释放操作,如果对象释放的次数超过了应有的次数,则这个错误在调用autorelease()时并不会被发现,只有当自动释放池被释放时(通常也就是游戏的每一帧结束时),游戏才会崩溃。在这种情况下,定位错误就变得十分困难了。例如,在游戏中,一个对象含有1个引用计数,但是却被调用了两次autorelease()。在第二次调用autorelease()时,游戏会继续执行这一帧,结束游戏时才会崩溃,很难及时找到出错的地点。 

n 因此,我们建议在开发过程中应该避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,其它情况尽量以release()来释放对象引用。 

 

容器

 

Cocos2d-x引擎为我们提供了CCArray、CCDictionary等Objective-C风格的容器。对C++标准库比较熟悉的读者可能疑惑,开发过程中为什么不直接使用vector等标准库已经提供的高效容器呢? 

n 使用Cocos2d-x容器的一个重要原因在于Cocos2d-x的内存管理。一般来说,被存入容器的对象在移除之前都应该保证是有效的,回顾一下引用计数的管理原则,对象的存入和移除必须对应一组retain()和release()或者对应autorelease()。直接使用STL容器,开发者势必进行烦琐重复的内存管理操作,而Cocos2d-x容器对这一过程进行了封装,保证了容器对对象的存取过程总是符合引用计数的内存管理原则。 

n 按照Cocos2d-x容器的内存管理要求,存入容器的对象必须是CCObject或其派生类。同时,Cocos2d-x的容器本身也是CCObject的派生类,当容器被释放时,它保存的所有元素都会被释放一次引用。CCArray的定义位于引擎目录下的

"cocos2dx\cocoa\CCArray.h(.cpp)"文件中

 

 

2.3.8 相关辅助宏 

n 引用计数很巧妙也很方便,但大部分处理过程涉及指针,难免比较烦琐,也容易出错。针对这个问题,Cocos2d-x为我们准备了一系列辅助宏来简化代码,这些宏都包含在头文件"CCPlatform Macro.h"里。表2-2列出了与内存管理相关的宏。 

n 表2-2 Cocos2d-x中与内存管理有关的宏 

 

l 宏 

 

 

l 描述 

 

 

n CC_SAFE_DELETE(p) 

 

 

l 使用delete操作符删除一个C++对象p, 

l 如果p为NULL,则不进行操作 

 

 

n CC_SAFE_DELETE_ARRAY(p) 

 

 

l 使用delete[]操作符删除一个C++数组p, 

l 如果p为NULL,则不进行操作 

 

 

n CC_SAFE_FREE(p) 

 

 

l 使用free()函数删除p,如果p为NULL, 

l 则不进行操作 

 

 

n CC_SAFE_RELEASE(p) 

 

 

l 使用release()方法释放Cocos2d-x对象p 的一次引用,如果p为NULL,则不进行操作 

 

 

 

n CC_SAFE_RELEASE_NULL(p) 

 

 

l 使用release()方法释放Cocos2d-x对象p的 

l 一次引用,再把p赋值为NULL。如果p已 

l 经为NULL,则不进行操作 

 

 

n CC_SAFE_RETAIN(p) 

 

 

l 使用reatin()方法增加Cocos2d-x对象p的一 

l 次引用。如果p为NULL,则不进行操作 

 

 

 

内存管理原则总结:

 

程序段必须成对执行retain()和release()或者执行autorelease()来声明开始和结束对象的引用;工厂方法返回前,应通过autorelease()结束对该对象的引用;

对象传值时,应考虑到新旧对象相同的特殊情况;尽量使用release()而不是autorelease()来释放对象引用,以确保性能最优;

保存CCObject的子类对象时,应严格使用Cocos2d-x提供的容器,避免使用STL容器,对象必须以指针形式存入。 

 如果希望自定义的类也拥有Cocos2d-x的内存管理功能,可以把CCObject作为自定义类的基类,并在实现类时严格遵守Cocos2d-x的内存管理原则。 

 

生命周期分析

控制游戏生命周期的AppDelegate文件,三个事件方法

//当应用程序重新活动中,会调用此方法

virtual void applicationWillEnterForeground();

//程序完成载入

    virtual bool applicationDidFinishLaunching();

//当应用程序不再活动时,会调用此方法。当手机接到电话时,它也会被调用。

virtual void applicationDidEnterBackground();

不过程序完成载入只有一次,且在applicationWillEnterForeground()之后。

 

由此可知,AppDelegate是整个程序的入口。实际上,AppDelegate处理了程序生命周期中的4个重要事件点:程序完成初始化,程序进入后台,程序重回前台和程序结束退出。进入后台和重回前台两个函数完成暂停和恢复游戏的处理。appDidFinishingLaunching则完成了加载游戏场景等工作。 

 


推荐阅读
author-avatar
camera98
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有