本文由子龙山人原创,原文链接:http://www.zilongshanren.com/cocos2d-x-design-pattern-singleton1/
cocos2d-x 官网: http://www.cocos.com/docs/html5/v3/singleton-objs/zh.html
1.Cocos2D-x中的单例如下:
CCDirector ,CCTextureCache, CCSpriteFrameCache, CCAnimationCache, CCUserDefault, CCNotificationCenter, CCShaderCache, CCScriptEngineManager, CCFileUtils, SimleAudioEngie
为什么会存在这样一些单例呢?
CCDirector单例:
它负责管理初始化OpenGL渲染窗口以及游戏场景的流程控制,它是cocos2dx游戏开发中必不可少的类之一。
为什么要把此类设计成单例对象呢?
因为,一个游戏只需要有一个游戏窗口就够了,所以,只需要初始化一次OpenGL渲染窗口。而且场景的流程控制功能,也只需要存在一个这样的场景控制对象即可。为了保证CCDirector类只存在一个实例对象,就必须使用单例模式。
CCTextureCache单例:
此类主要负责加载游戏当中所需要的纹理图片资源,这些资源加载好以后,就可以一直保留在内存里面,当下次再需要使用此纹理的时候,直接返回相应的纹理对象引用就可以了,无需再重复加载。当然,这样做可能会很浪费内存,所以cocos2dx采用了一种引用计数的方式来管理对象内存,当纹理刚被加载进来的时候,引用计数为1。如果使用此纹理对象创建一个精灵,那么此纹理对象引用会加1.如果精灵被释放,则相应的引用计数减1.当纹理的引用计数变为0的时候,纹理所占用的内存自然就会被释放掉。这也是为什么在收到内存警告的时候,会调用CCTextureCache的removeUnusedTextures方法。此方法会将所有引用计数为1的纹理对象全部释放掉。单从字面上看,Cache,即缓存的意思。它以牺牲一定的内存压力为代价,带来的是游戏性能的提升。这种cache技术,在游戏开发中比比皆是。注:IO操作对游戏性能影响非常大,要极力避免!!!
扩展:
类似的CCSpriteFrameCache、CCAnimationCache和CCShaderCache,它们也都是缓存类,分别负责缓存SpriteFrame、Animation和Shader。这样做的原因无非就是为了性能,以空间换时间.
CCUserDefault单例:
此类主要是用来保存游戏中的数据用的,它会创建一个xml文件,并把用户自定义的数据以key-value的形式存储到此xml文件中。此类为什么会变成单例类呢?原因也很简单,因为类似这种操作数据文件,或者配置文件的类,通常只需要在程序运行过程中存在一个实例即可。
CCNotificationCenter单例:
这是一个通知中心,它其实还运用了一个观察者模式,这里暂时不讨论。该通知中心理论上也是只需要一个就够了。但是,cocos2d-x在实现此单例的时候,并没有将此类的构造函数私有么,我在猜想,是不是开发人员有意为之呢?或者多个通知中心也有其存在的价值。这个大家可以讨论一下。
CCScriptEngineManager单例:
此类包含一个实现了CCScriptEngineProtocl接口的对象引用,它可以帮助我们方便地找到LuaEngine对象。这里单例的作用纯粹变成了LuaEngine的一个全局访问点了。
为什么不直接把LuaEngine作为单例对象呢?
是否在某些情况之下,可能需要创建多个LuaEngine对象?如果考虑到cocos2d-x还可以同时支持其它的脚本引擎,那也可以相应的把另外的脚本引擎设计成单例类。当然,这样做无疑会使引擎里面的单例过多。考虑到单例模式近年来被广大开发者所诟病,已将其列入“反模式”。这里引用CCScriptEngineManager单例类,给其它引擎对象提供访问的惟一全局点,这也不失为一个办法。不知我的推测是否正确?
CCFileUtils类:
该类是一个工具类。工具类和配置文件类,它们绝大多数情况也都是设计成单例的。因为它们没有存在多个实例的必要。同时,它们也可以实现为一组类方法,这样无需创建对象也能够使用。
SimpleAudioEngine类:
它也被设计成了一个单例类。因为它提供给了开发人员最简单的声音操作接口,可以方便地处理游戏中的背景音乐和音效。此类同时还应用了外观模式,把CocoDenshion子系统中的复杂功能给屏蔽起来了,简化了客户端程序员的调用。该类为什么要设计成单例,是因为到处都要访问它。设计成单例会很方便,而且它与其它对象没有什么联系,不好使用对象组合。
2.使用单例模式的优缺点
优点:简单易用,限制一个类只有一个实例,可以减少创建多个对象可能会引起的内存问题的风险,包括内存泄漏、内存占用问题。
缺点:单例模式因为提供了一个全局的访问点,你可以在程序的任何地方轻而易取地访问到,这本身就是一种高耦合的设计。一旦单例改变以后,其它模板都需要修改。另外,单例模式使得对象变成了全局的了。学过面对对象编程的人都知道,全局变量是非常邪恶的,要尽量不要使用。而且单例模式会使得对象的内存在程序结束之前一直存在,在一些使用GC的语言里面,这其实就是一种内存泄漏,因为它们永远都不到释放。当然,也可以通过提供一些方法来释放单例对象所占用的内存,比如前面提到的XXXCache对象,都有相应的Purge方法。最后,cocos2dx里面实现的单例,99%都不是线程安全的。
在讨论优缺点的时候,读者想必也看出来了,缺点比优点多多了。这是给大家提个醒,以后使用单例模式的时候要谨慎,不要滥用。因为此模式最容易被滥用。只有真正符合单例模式应用场景的时候,才能考虑。不要为了访问方便,就把任何类都弄成单例,这样,到最后,你会发现你的程序里面就只剩下一堆单例和工厂了。此外,单例模式正在消减,比如CCActionManager和CCTouchDispatcher在cocos2d1.0之前也是单例,现在变成了CCDirector类的属性了。而且Riq(cocos2d-iphone的作者)也有在邮件中提到,以后CCDirector对象也会变成非单例,并且允许一个游戏中创建多个游戏窗口。
3.单例模式的定义
public class Singleton
{
public:
//全局访问点
static Singleton* SharedSingleton()
{
if(NULL == m_spSingleton)
{
m_spSingleton = new Singleton();
}
return m_spSingleton;
}
private:
static Singleton* m_spSingleton;
Singleton();
Singleton(const Singleton& other);
Singleton& operator=(const Singleton& other);
};
Singleton* Singleton::m_spSingleton = NULL;
注意,这里只是最基本的实现,它没有考虑到线程安全,也没有考虑内存释放。但是,这个实现有两个最基本的要素。一:定义一个静态变量,并把构造函数等设置为私有的。二:提供一个全局的访问点给外部访问。
4.游戏开发中如何运用此模式呢?
众所周知,游戏开发中离不开游戏数据保存和加载。这些数据包括关卡数据、游戏进行中的状态数据等。这样一些信息很多游戏模块中都需要访问,所以可以为之设置一个单例对象。我武断地认为,客户端游戏开发中,至少需要一个单例对象。因为一个全局的访问点可以方便很多对象之间的交互。根据之前的讨论,也可以把一些时觉需要用到的类引用保存在此单例对象中,不过只需要保存弱引用即可。使用单例,最严重的就是怕内存泄漏,所以,大家尽量不要把单例类设计地太复杂,也不要让它包含过多的动态内存管理工作。
二段构建模式
所谓二段构建,就是指创建对象时不是直接通过构建函数来分配内存并完成初始化操作。取而代之的是,构造函数只负责分配内存,而初始化的工作则由一些名为initXXX的成员方法来完成。然后再定义一些静态类方法把这两个阶段组合起来,完成最终对象的构建。因为在《Cocoa设计模式》一书中,把此惯用法称之为“Two Stage Creation”,即“二段构建”。因为此模式在cocos2d里面被广泛使用,所以把该模式也引入过来了。
1.应用场景:
二段构建在cocos2d-x里面随处可见,自从2.0版本以后,所有的二段构建方法的签名都改成create了。这样做的好处是一方面统一接口,方便记忆,另一方面是以前的类似Cocoa的命名规范不适用c++,容易引起歧义。下面以CCSprite为类,来具体阐述二段构建的过程,请看下列代码:
//此方法现在已经不推荐使用了,将来可能会删除
CCSprite* CCSprite::spriteWithFile(const char pszFileName)
{
return CCSprite::create(pszFileName);
}
CCSprite CCSprite::create(const char *pszFileName)
{
CCSprite *pobSprite = new CCSprite(); //1.第一阶段,分配内存
if (pobSprite && pobSprite->initWithFile(pszFileName)) //2.第二阶段,初始化
{
pobSprite->autorelease(); //!!!额外做了内存管理的工作。
return pobSprite;
}
CC_SAFE_DELETE(pobSprite);
return NULL;
}
如上面代码中的注释所示,创建一个sprite明显被分为两个步骤:1.使用new来创建内存;2.使用initXXX方法来完成初始化。
因为CCSprite的构造函数也有初始化的功能,所以,我们再来看看CCSprite的构建函数实现:
CCSprite::CCSprite(void)
: m_pobTexture(NULL)
, m_bShouldBeHidden(false)
{
}
很明显,这个构建函数所做的初始化工作非常有限,仅仅是在初始化列表里面初始化了m_pobTexture和m_bShouldBeHidden两个变量。实际的初始化工作大部分都放在initXXX系列方法中,大家可以动手去查看源代码。
2.分析为什么要使用此模式?
这种二段构建对于C++程序员来说,其实有点别扭。因为c++的构造函数在设计之初就是用来分配内存+初始化对象的。如果再搞个二段构建,实则是多此一举。但是,在objective-c里面是没有构造函数这一说的,所以,在Cocoa的编程世界里,二段构建被广泛采用。而cocos2d-x当初是从cocos2d-iphone移植过来了,为了保持最大限度的代码一致性,所以保留了这种二段构建方式。这样可以方便移植cocos2d-iphone的游戏,同时也方便cocos2d-iphone的程序员快速上手cocos2d-x。不过在后来,由于c++天生不具备oc那种可以指定每一个参数的名称的能力,所以,cocos2d-x的设计者决定使用c++的函数重载来解决这个问题。这也是后来为什么2.0版本以后,都使用create函数的重载版本了.
虽然接口签名改掉了,但是本质并没有变化,还是使用的二段构建。二段构建并没有什么不好,只是更加突出了对象需要初始化。在某种程度上也可以说是一种设计强化。因为忘记初始化是一切莫名其妙的bug的罪魁祸首。同时,二段构建出来的对象都是autorelease的对象,而autorelease对象是使用引用计数来管理内存的。客户端程序员在使用此接口创建对象的时候,无需关心具体实现细节,只要知道使用create方法可以创建并初始化一个自动释放内存的对象即可。
在一点,在《Effective Java》一书中,也有提到。为每一个类提供一个静态工厂方法来代替构造函数,
它有以下三个优点:
1.与构造函数不同,静态方法有名字,而构造函数只能通过参数重载。
2.它每次被调用的时候,不一定都创建一个新的对象。比如Boolean.valueOf(boolean)。
3.它还可以返回原类型的子类型对象。
因此,使用二段构建的原因有二:
1.兼容性、历史遗留原因。(这也再次印证了一句话,一切系统都是遗留系统,呵呵)
2.二段构建有其自身独有的优势。
3.使用此模式的优缺点是什么?
优点:
1.显示分开内存分配和初始化阶段,让初始化地位突出。因为程序员一般不会忘记分配内存,但却常常忽略初始化的作用。
2.见上面分析《Effective Java》的第1条:“为每一个类提供一个静态工厂方法来代替构造函数”
3.除了完成对象构建,还可以管理对象内存。
缺点:
1.不如直接使用构造函数来得直白、明了,违反直觉,但这个是相对的。
4.此模式的定义及一般实现定义:将一个对象的构建分为两个步骤来进行:1.分配内存 2.初始化它的一般实现如下:
class Test
{
public:
//静态工厂方法
static Test* create()
{
Test *pTest = new Test;
if (pTest && pTest->init()) {
//这里还可以做其它操作,比如cocos2d-x里面管理内存
return pTest;
}
return NULL;
}
//
Test()
{
//分配成员变量的内存,但不初始化
}
bool init(){
//这里初始化对象成员
return true;
}
private:
//这里定义数据成员
};
5.在游戏开发中如何运用此模式
5.在游戏开发中如何运用此模式这个也非常简单,就是今后在使用cocos2d-x的时候,如果你继承CCSprite实现自定义的精灵,你也需要按照“二段构建”的方式,为你的类提供一个静态工厂方法,同时编写相应的初始化方法。当然,命名规范最好和cocos2d-x统一,即静态工厂方法为create,而初始化方法为initXXXX。
6.此模式经常与哪些模式配合使用
由于此模式在GoF的设计模式中并未出现,所以暂时不讨论与其它模式的关系。最后看看cocos2d-x创始人王哲对于为什么要设计成二段构建的看法:“其实我们设计二段构造时首先考虑其优势而非兼容cocos2d-iphone. 初始化时会遇到图片资源不存在等异常,而C++构造函数无返回值,只能用try-catch来处理异常,启用try-catch会使编译后二进制文件大不少,故需要init返回bool值。Symbian, Bada SDK,objc的alloc + init也都是二阶段构造”