本系列文章将按照如下思路进行模式挖掘:
– 分析为什么要使用此模式(即设计决策)
– 使用此模式的优缺点是什么(任何事务都有两面性,设计模式也不例外)
– 在游戏开发中如何运用此模式(自己对于模式运用场景的理解)
– 此模式经常与哪些模式配合使用(这个也基本是从GoF的书籍里面)
应用场景
Cocos2D-x 3.0 中的单例如下:Director,TextureCache,SpriteFrameCache,AnimationCache,UserDefault,GLProgramStateCache,ScriptEngineManager,PoolManager,FileUtils,Profiler,SimleAudioEngie,Configuration,Application,GLProgramCache
为什么会存在这样一些单例呢?
Director单例,它负责管理初始化OpenGL渲染窗口以及游戏场景的流程控制,它是cocos2dx游戏开发中必不可少的类之一。为什么要把此类设计成单例对象呢?因为,一个游戏只需要有一个游戏窗口就够了,所以,只需要初始化一次OpenGL渲染窗口。而且场景的流程控制功能,也只需要存在一个这样的场景控制对象即可。为了保证Director类只存在一个实例对象,就必须使用单例模式。但是如果我们需要在一个应用当中嵌套多个Cocos2D-X的实例,那么Director类是不能作为单例的。
TextureCache单例。此类主要负责加载游戏当中所需要的纹理图片资源,这些资源加载好以后,就可以一直保留在内存里面,当下次再需要使用此纹理的时候,直接返回相应的纹理对象引用就可以了,无需再重复加载。当然,这样做可能会很浪费内存,所以cocos2dx采用了一种引用计数的方式来管理对象内存,当纹理刚被加载进来的时候,引用计数为1。如果使用此纹理对象创建一个精灵,那么此纹理对象引用会加1.如果精灵被释放,则相应的引用计数减1.当纹理的引用计数变为0的时候,纹理所占用的内存自然就会被释放掉。这也是为什么在收到内存警告的时候,会调用TextureCache的removeUnusedTextures方法。此方法会将所有引用计数为1的纹理对象全部释放掉。单从字面上看,Cache,即缓存的意思。它以牺牲一定的内存压力为代价,带来的是游戏性能的提升。这种cache技术,在游戏开发中比比皆是。注:IO操作对游戏性能影响非常大,要极力避免!!!
类似的SpriteFrameCache、AnimationCache和GLProgramCache,它们也都是缓存类,分别负责缓存SpriteFrame、Animation和GLProgram。这样做的原因无非就是为了性能,以空间换时间。
UserDefault 此类主要是用来保存游戏中的数据用的,它会创建一个xml文件,并把用户自定义的数据以key-value的形式存储到此xml文件中。此类为什么会变成单例类呢?原因也很简单,因为类似这种操作数据文件,或者配置文件的类,通常只需要在程序运行过程中存在一个实例即可。但是,我们真的需要这个单例吗?我们实际开发游戏的过程中,一般也会有自己的数据持久化方法,通常也会被设计成单例类。其实如果只是几个数据持久化方法,完全可以用几个匿名名字空间中的函数来解决。
ScriptEngineManager,此类包含一个实现了ScriptEngineProtocl接口的对象引用,它可以帮助我们方便地找到LuaEngine对象。这里单例的作用纯粹变成了LuaEngine的一个全局访问点了。为什么不直接把LuaEngine作为单例对象呢?是否在某些情况之下,可能需要创建多个LuaEngine对象?如果考虑到cocos2d-x还可以同时支持其它的脚本引擎,那也可以相应的把另外的脚本引擎设计成单例类。当然,这样做无疑会使引擎里面的单例过多。考虑到单例模式近年来被广大开发者所诟病,已将其列入“反模式”。这里引用CCScriptEngineManager单例类,给其它引擎对象提供访问的惟一全局点,这也不失为一个办法。不知我的推测是否正确?
PoolManager,此类是用来管理AutoReleasePool对象栈的。因为cocos2d-x采用的是基于引用计数的方式来管理动态内存,所以采用引用计数的被托管对象都被放入一个当前的autoReleasePool里面去了。当Director的mainLoop每次更新的时候,都会调用PoolManager的pop方法,把当前autoReleasePool里面的所有autoRelease对象的托管状态设置为false,同时把该autoReleasePool清空,而清空的时候则会调用autoReleasePool里面所有对象的release方法来释放内存。此类为什么要设计与单例呢?因为多个地方需要引用此类,为了方便引用,所以设计成单例。
FileUtils类。该类是一个工具类。工具类和配置文件类,它们绝大多数情况也都是设计成单例的。因为它们没有存在多个实例的必要。同时,它们也可以实现为一组类方法,这样无需创建对象也能够使用。
Profiler类,该类负责cocos2d的性能其运行情况分析,也是一个工具类。所以它设计成单例类的理由与FileUtils类差不多。这个单例也显得有点多余,用名字简单+一些全局函数就可以解决了。
Configuration类也被设计成了单例对象,此类主要负责管理cocos2d-x的一些OpenGL变量信息。这些信息本可以通过定义一些宏,或者通过一些全局变量来解决的。这里设计成单例类也是更加“面向对象”的体现。因为这些配置信息根本不需存在多个对象。此类,完全可以变成Director类的一个内部私有类,然后通过Director来提供访问点。
Application类的设计初衷是获得平台相关的一些信息,最重要的是运行游戏的主循环(main loop)。一个游戏只需要一个应用程序实例,所以设计与单例。
SimpleAudioEngine类,它也被设计成了一个单例类。因为它提供给了开发人员最简单的声音操作接口,可以方便地处理游戏中的背景音乐和音效。此类同时还应用了外观模式,把CocoDenshion子系统中的复杂功能给屏蔽起来了,简化了客户端程序员的调用。该类为什么要设计成单例,是因为到处都要访问它。设计成单例会很方便,而且它与其它对象没有什么联系,不好使用对象组合。
使用单例模式的优缺点
优点:
1)简单易用,限制一个类只有一个实例,可以降低创建多个对象可能会引起的内存问题的风险,包括内存泄漏、内存占用问题。
缺点:
单例模式因为提供了一个全局的访问点,你可以在程序的任何地方轻而易取地访问到,这本身就是一种高耦合的设计。一旦单例改变以后,其它模块都需要修改。另外,单例模式使得对象变成了全局的了。学过面对对象编程的人都知道,全局变量是非常邪恶的,要尽量不要使用。而且单例模式会使得对象的内存在程序结束之前一直存在,在一些使用GC的语言里面,这其实就是一种内存泄漏,因为它们永远都不到释放。当然,也可以通过提供一些特殊的方法来释放单例对象所占用的内存,比如前面提到的XXXCache对象,都有相应的Purge方法。最后,cocos2dx里面实现的单例,99%都不是线程安全的。
在讨论优缺点的时候,读者想必也看出来了,缺点比优点多多了。这是给大家提个醒,以后使用单例模式的时候要谨慎,不要滥用。因为此模式最容易被滥用。只有真正符合单例模式应用场景的时候,才能考虑。不要为了访问方便,就把任何类都弄成单例,这样,到最后,你会发现你的程序里面就只剩下一堆单例和工厂了。
此外,单例模式正在消减,比如ActionManager和TouchDispatcher在cocos2d1.0之前也是单例,现在变成了Director类的属性了。而且Riq(cocos2d-iphone的作者)也有在邮件中提到,以后Director对象也会变成非单例,并且允许一个游戏中创建多个游戏窗口。
大多数时候,我们在考虑实现一个功能的时候,首先想到的一定不要是单例。单例就像全局变量,它会散布在你的程序的各个地方。推荐尽可能地用类函数,名字空间+全局函数,或者内部类来解决。而且使用单例实际上是一种程序员精神懒惰的表现,不想去管理内存,因为想不清楚内存什么时候该释放。反正我只有一个对象,这点内存不算什么。反正我以后对单例是无爱了,大多数使用单例的场景,其实都可以有替代方案。
单例模式的定义
保证一个类仅有一个实例,并提供一个访问它的全局的访问点。
代码片段:
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;
注意,这里只是最基本的实现,它没有考虑到线程安全,也没有考虑内存释放。但是,这个实现有两个最基本的要素。一:定义一个静态变量,并把构造函数等设置为私有的。二:提供一个全局的访问点给外部访问。
游戏开发中如何运用此模式呢?
众所周知,游戏开发中离不开游戏数据保存和加载。这些数据包括关卡数据、游戏进行中的状态数据等。这样一些信息很多游戏模块中都需要访问,所以可以为之设置一个单例对象。我武断地认为,客户端游戏开发中,至少需要一个单例对象。因为一个全局的访问点可以方便很多对象之间的交互。根据之前的讨论,也可以把一些时觉需要用到的类引用保存在此单例对象中,不过只需要保存弱引用即可。使用单例,最严重的就是怕内存泄漏,所以,大家尽量不要把单例类设计地太复杂,也不要让它包含过多的动态内存管理工作。
与其它模式的关系
单例模式一般与工厂模式配合使用,因为一般会将工厂类设计成单例对象。
比如前面提到的各种cache类,它们也可以看作是某种意义上的工厂对象。由于工厂就是负责生产对象的,而cache类都可以根据用户的需要生产出相应的对象。
最后,看看cocos2d-x创始人王哲对于什么是单例的看法:“这么说吧, 我设计成单例基本就一种抽象情况:独占性资源。比如某个硬件IO (如TouchDispatcher, Accelerometer),比如公用的缓存区(TextureCache, UserDefault)。后来有人抱怨单例类太多,想销毁整个cocos2d instance再重建很麻烦,所以小明和riq就把大量单例类放到Director里面管理。”
那到底什么时候该使用Singleton,什么时候要避免使用Singleton呢?
考虑使用的场景
这个类必须只能有一个实例,且必须只能让客户端通过一个全局的访问点来访问。比如一个日志类。但是日志类可以用面向方面的编程来解决,而且这种解决问题的方法比单例更优雅。
不该使用的场景 。
几乎任何场景都不要优先想到单例,虽然使用它刚开始会带来不少好处。但是,随着时间的推移,它的各种弊端就开始显现出来了。另外,在多线程环境中,单例也是个坑。几乎很少有人可以正确地写出一个线程安全的单例类。