Unity3D单例模式和静态类的使⽤详解
Unity3D的API提供了很多的功能,但是很多流程还是会⾃⼰去封装⼀下去。当然现在⽹上也有很多的框架可以去下载使⽤,但是肯定不会⽐⾃⼰写的⽤起来顺⼿。
对于是否需要使⽤框架的问题上,本⼈是持肯定态度的,把⼀些常⽤⽅法进⾏封装,做成⼀个功能性的框架,可以很⼤程度上提⾼代码的效率,维护也⽅便。
对于⽹络上很多教程上使⽤的“游戏通⽤MVC框架”,现在看来并不符合MVC这种结构性框架的设计思想:要知道,MVC最初是被设计为Web应⽤的框架,⽽游戏中的很多事件并不是通过⽤户点击UI发⽣的,View和Controller在游戏逻辑中的占⽐⼀般都少的可怜,⽽且很多教程上把Model剥离出很多“Manager”模块,甚⾄有⼈把View和Controller合在⼀起写了UIManager——连MVC的结构都没了,为啥还要称之为MVC框架呢?
MVC: “⼈红是⾮多。。。。”
⽬前⼤部分的游戏框架——特别是⼩型项⽬的游戏框架——都是把⼀些数据的特定⾏为进⾏了⼀下封装:⽣成⼀个物件,播放⼀个特效,进⾏⼀次随机事件等。当然也会有⼀些结构性的设计或者资源管理设计如:UI的回退栈或者回退链,场景的载⼊记录和切换,下载队列的管理等。
在Unity的框架设计中,有⼀个词会经常见到:单例模式(singleton)。单例模式就是在整个游戏中只使⽤某个类的⼀个实例,核⼼的⼀句话就是public static T Instance;即在类中定义了⼀个静态的⾃⾝实例供外部使⽤,调⽤⽅法时就是:
T.Instance.Function()。在本⼈最初接触这种设计⽅式时经常会与静态类弄混淆,T.Function()。中间差了⼀个静态Instance,很多时候好像区别不⼤。。。
在接近两周左右的时间⾥,我⼀直在纠结于⾃⼰正在写的框架到底应该写成单例模式的还是静态模式的,今天刚好对这个问题有了⼀个新的想法:静态可不可以理解为⼀种封闭性很强的单例?
⾸先回想⼀下静态的两个常识:
unity 教程
1、静态类不能继承和被继承!(严格点说是只能继承System.Object)也就是说你的静态类不可能去继承MonoBehaviour,不能实现接⼝。
2、静态⽅法不能使⽤⾮静态成员!如果你⼤量使⽤静态⽅法,⽽⽅法⾥⼜需要⽤到这个类的成员,那么你的成员得是静态成员。
第2点需要注意:如果你想在Unity的编辑器下调整某个参数,那么这个参数就不能是静态的(哪怕你⾃定义EditorWindow去修改这个值也没⽤),解决的办法是通过UnityEngine.ScriptableObject去存放
配置(⽣成*.asset⽂件),然后在运⾏中通过LoadAsset去加载,然后再改变静态成员。⾄于原因,相信不难理解——你看到的所有Unity组件都是⼀个个实例,你要通过Unity的编辑器去配置,那么你就得有⼀个这样的可配置实例。
从⾯向对象上想⼀下:静态⽅法或者静态类,不需要依赖对象,类是唯⼀的;单例的静态实例,⼀般就是唯⼀的⼀个对象(当然也可以有多个)。差别嘛。。。好像也不⼤。。。
如果这样考虑没有错,那再回头⽐较⼀下两种⽅式:
1、静态(静态⽅法或者静态类),代码编写上绊⼿绊脚,⽅法调⽤很⽅便,运⾏效率⾼⼀丢丢。逻辑⾯向过程,不能很好地控制加载和销毁。
2、单例(类的静态实例),代码编写和其他类完全⼀样,继承抽象模版接⼝都可以,Unity⾥也很⽅便进⾏参数配置,不过使⽤⿇烦有犯错的可能性(必须通过实例调⽤⽅法),效率不如静态(但是也不会有很⼤影响吧)。
如果这些说法太抽象,那我再给出⼀个常见的问题:如果你的框架有⼀个SoundManager能够管理所有的声⾳播放,那么你会怎么去实现?
(在刚接触AudioSource这个组件的时候,我想的是每⼀个声⾳都由⼀个AudioSource去播放。但是后
来发现完全没必
要,AudioSource有静态的PlayClipAtPoint⽅法去播放临时3D⾳效,同时有实例⽅法PlayOneShot去播放临时⾳效(2D和3D 取决于当实例的SpatialBlend)。如果没有特殊的需求,那么⼀个AudioSource循环播放背景⾳乐,上述两种⽅法播放游戏中的特效⾳频,这对于⼤部分游戏已经⾜够了。)
那么问题来了:你的SoundManager播放声⾳的⽅法如果是静态的,那么AudioSource组件必须在代码中通过各种⽅式去获取(新建组件或者获取特定GameObject下的组件)——因为保存这个组件的变量必须是静态的,也就不能通过Unity的编辑器去赋值。如果不去阅读代码那么⽤户完全不知道这是⼀个什么样的组件获取流程,如果我破坏这个流程(同名物体,包含互斥组件等),那么这个Manager很有可能会出现不可预料的异常。
⽽继承MonoBehaviour并RequireComponent(typeof(AudioSource)),怎么看也⽐“为了静态⽽静态”的代码要⽅便健壮的多。实际上到这⾥已经可以基本总结出何时需要使⽤单例了:
1、只要你的类需要保存其他组件作为变量,那么就有必要使⽤单例;
2、只要你有在Unity编辑器上进⾏参数配置的需求,那么就有必要使⽤单例;
3、只要你的管理器需要进⾏加载的顺序控制,那么就有必要使⽤单例(⽐如热更新之后加载ResourcesManager);
当然,这⾥都只是“有必要”,并不是“必须”。两者区别最⼤的地⽅,⼀个是⽅便写,⼀个是⽅便⽤。⽅便写的代价是每次调⽤加个instance,⽅便⽤的代价则是放弃了⾯向对象和Unity的“所见即所得”,孰轻孰重,⾃⼰抉择。
另⼀⽅⾯,和“为了静态⽽静态”⼀样,“为了单例⽽单例”同样是⼀个不合理的设计。这样的解释仍然是那么的模糊,那么,就给⾃⼰定义⼀个最简单的规则吧——如果你的单例类⾥没有任何需要保存状态的变量,那么这个类⾥的⽅法就可以全都是静态⽅法,这个类也可以是个静态类。
补充:从实例出发,了解单例模式和静态块
就算你没有⽤到过其他的设计模式,但是单例模式你肯定接触过,⽐如,Spring 中 bean 默认就是单例模式的,所有⽤到这个bean 的实例其实都是同⼀个。
单例模式的使⽤场景
什么是单例模式呢,单例模式(Singleton)⼜叫单态模式,它出现⽬的是为了保证⼀个类在系统中只有⼀个实例,并提供⼀个访问它的全局访问点。从这点可以看出,单例模式的出现是为了可以保证系统
中⼀个类只有⼀个实例⽽且该实例⼜易于外界访问,从⽽⽅便对实例个数的控制并节约系统资源⽽出现的解决⽅案。
使⽤单例模式当然是有原因,有好处的了。在下⾯⼏个场景中适合使⽤单例模式:
1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
2、创建对象时耗时过多或者耗资源过多,但⼜经常⽤到的对象;
3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地⽂件;
下⾯举⼏个例⼦来说明⼀下:
1、⽹站在线⼈数统计;
其实就是全局计数器,也就是说所有⽤户在相同的时刻获取到的在线⼈数数量都是⼀致的。要实现这个需求,计数器就要全局唯⼀,也就正好可以⽤单例模式来实现。当然这⾥不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。下⾯代码是⼀个简单的计数器实现。
public class Counter {
private static class CounterHolder{
private static final Counter counter = new Counter();
}
private Counter(){
System.out.println("");
}
public static final Counter getInstance(){
unter;
}
private AtomicLong online = new AtomicLong();
public long getOnline(){
();
}
public long add(){
return online.incrementAndGet();
}
}
2、配置⽂件访问类;
项⽬中经常需要⼀些环境相关的配置⽂件,⽐如短信通知相关的、邮件相关的。⽐如 properties ⽂件,这⾥就以读取⼀个properties ⽂件配置为例,如果你使⽤的 Spring ,可以⽤ @PropertySource 注解实现,默认就是单例模式。如果不⽤单例的话,每次都要 new 对象,每次都要重新读⼀遍配置⽂件,很影响性能,如果⽤单例模式,则只需要读取⼀遍就好了。以下是
⽂件访问单例类简单实现:
public class SingleProperty {
private static Properties prop;
private static class SinglePropertyHolder{
private static final SingleProperty singleProperty = new SingleProperty();
}
/**
* config.properties 内容是 test.name=kite
*/
private SingleProperty(){
System.out.println("构造函数执⾏");
prop = new Properties();
InputStream stream = ClassLoader()
.getResourceAsStream("config.properties");
try {
prop.load(new InputStreamReader(stream, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SingleProperty getInstance(){
return SinglePropertyHolder.singleProperty;
}
public String getName(){
("test.name").toString();
}
public static void main(String[] args){
SingleProperty singleProperty = Instance();
System.out.Name());
}
}
3、数据库连接池的实现,也包括线程池。
为什么要做池化,是因为新建连接很耗时,如果每次新任务来了,都新建连接,那对性能的影响实在太⼤。所以⼀般的做法是在⼀个应⽤内维护⼀个连接池,这样当任务进来时,如果有空闲连接,可以直接拿来⽤,省去了初始化的开销。
所以⽤单例模式,正好可以实现⼀个应⽤内只有⼀个线程池的存在,所有需要连接的任务,都要从这个连接池来获取连接。
如果不使⽤单例,那么应⽤内就会出现多个连接池,那也就没什么意义了。如果你使⽤ Spring 的话,并集成了例如 druid 或者 c3p0 ,这些成熟开源的数据库连接池,⼀般也都是默认以单例模式实现的。
单例模式的实现⽅法
如果你在书上或者⽹站上搜索单例模式的实现,⼀般都会介绍5、6中⽅式,其中有⼀些随着 Java 版本的升⾼,以及多线程技术的使⽤变得不那么实⽤了,这⾥就介绍两种即⾼效,⽽且⼜是线程安全的⽅式。
1. 静态内部类⽅式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法仍然使⽤ JVM 本⾝机制保证了线程安全问题,由于 SingletonHolder 是私有的,除了 getInstance() ⽅法外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进⾏同步,没有性能缺陷;也不依赖 JDK 版本。上⾯的两个例⼦就是⽤这种⽅式实现的。
2. 枚举⽅式
public enum SingleEnum {
INSTANCE;
SingleEnum(){
System.out.println("构造函数执⾏");
}
public String getName(){
return "singleEnum";
}
public static void main(String[] args){
SingleEnum singleEnum = SingleEnum.INSTANCE;
System.out.Name());
}
}
我们可以通过 SingleEnum.INSTANCE 来访问实例。⽽且创建枚举默认就是线程安全的,并且还能防⽌反序列化导致重新创建新的对象。
静态块
什么是静态块呢
1、它是随着类的加载⽽执⾏,只执⾏⼀次,并优先于主函数。具体说,静态代码块是由类调⽤的。类调⽤时,先执⾏静态代码块,然后才执⾏主函数的;
2、静态代码块其实就是给类初始化的,⽽构造代码块是给对象初始化的;
3、静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别;
4、⼀个类中可以有多个静态代码块;
他的写法是这样的:
static {
System.out.println("static executed");
}
来看⼀下下⾯这个完整的实例:
public class SingleStatic {
static {
System.out.println("static 块执⾏中...");
}
{
System.out.println("构造代码块执⾏中...");
}
public SingleStatic(){
System.out.println("构造函数执⾏中");
}
public static void main(String[] args){
System.out.println("main 函数执⾏中");
SingleStatic singleStatic = new SingleStatic();
}
}
他的执⾏结果是这样的:
static 块执⾏中...
main 函数执⾏中
构造代码块执⾏中...
构造函数执⾏中
从中可以看出他们的执⾏顺序分别为:
1、静态代码块
2、main 函数
3、构造代码块
4、构造函数
利⽤静态代码块只在类加载的时候执⾏,并且只执⾏⼀次这个特性,也可以⽤来实现单例模式,但是不是懒加载,也就是说每
次类加载就会主动触发实例化。
除此之外,不考虑单例的情况,利⽤静态代码块的这个特性,可以实现其他的⼀些功能,例如上⾯提到的配置⽂件加载的功能,可以在类加载的时候就读取配置⽂件的内容,相当于⼀个预加载的功能,在使⽤的时候可以直接拿来就⽤。
以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。如有错误或未考虑完全的地⽅,望不吝赐教。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。