单例模式

单例模式,即当前场景中有且只有一个的对象组件。通常单例被做成 AudioManagerGameManager 这种功能

特性

  • 保证一个类仅有一个实例,也有生命周期
  • 提供一个全局访问的接口,任何地方都可以获取到

关于单例的风险:

  • 如果单例使用懒汉模式,那么其初始化就是不可控的,因为在第一次使用时才会创建
  • 由于可以全局访问,逻辑散落到各处,因此逻辑之间的引用就可能会非常混乱
  • 因为可以通过 Instance 获取实例对象,所以内部的成员变量就会暴露出来,从而带来被修改的风险

懒汉

懒汉模式即程序延迟创建对象,只有在程序第一次调用此单例对象时才会创建,由此就导致了可能会有多个线程同时访问导致线程安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 非线程安全
public sealed class Singleton
{
private static Singleton instance = null;

public static Singleton Instance {
get {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}

此处的 instance == null 在多个线程中判断并不准确,会导致创建多个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 简单的线程安全写法
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();

public static Singleton Instance {
get {
lock (padlock) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
}

此时线程安全了,但是性能上却出现了问题,因为每次获取无论实例有没有创建都在上锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 双检查锁
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();

public static Singleton Instance {
get {
if (instance == null) {
lock (padlock) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
}

当第一次判断 instance == null 的时候,有可能另外一条线程正在对其实例化,但是还没有完成,因此进入锁之后还继续判断了一次 instance == null

注意,此处的双检查锁实现并不能保证线程安全问题,因为 c# 中的指令重排问题:instance = new Singleton(); 这行代码其实是分为了三个步骤:

  • (1) 为 Singleton 分配内存空间;
  • (2) 调用 Singleton 的构造函数,初始化成员字段;
  • (3) 将 instance 对象指向分配的内存空间。

本来程序是按照 1-2-3 的指令顺序执行,但是因为指令重排优化导致其执行顺序变成了 1-3-2。那么此时就会引发一个问题:

  • Thread A 检查 instancenull ,此时进入第一个 if 语句
  • Thread A 获得锁
  • Thread A 发现 instance 仍然为 null ,于是创建一个新对象但还未初始化
  • 由于指令重排,此时 instance 被赋值,但对象尚未初始化
  • Thread B 检查 instance 不为 null ,直接返回未初始化的 instance

解决办法:给 instance 加上 volatile 关键字避免对其指令重排优化

1
private static volatile Singleton instance = null;

饿汉

饿汉就是在程序一开始就初始化唯一实例,不会有线程安全问题

1
2
3
4
5
6
7
8
9
10
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();

public static Singleton Instance {
get {
return instance;
}
}
}

MonoBehaviour

对于 unityMonoBehaviour ,因为其本来就是单线程的,因此也不用考虑多线程资源争抢的问题。可以直接实现一个对于其的泛型单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
public static T Instance { get; private set; }

protected virtual void Awake() {
if (Instance != null && Instance != this) {
// Destroy(gameObject);
Debug.LogError("Multiple instances of Singleton detected. Please fix this issue.");
} else {
Instance = (T)this;
// DontDestroyOnLoad(gameObject);
}
}

public static bool IsInitialized() {
return Instance != null;
}

protected virtual void OnDestroy() {
if (Instance == this) {
Instance = null;
}
}
}

其中有两处为根据自己需求的可选项:

  • 在发现有多个实例的时候销毁或者直接LogError崩掉
  • 可以让此单例对象在加载场景时是否销毁

观察者模式

观察者模式是一种一对多的依赖关系,使得当一个对象(被观察者)的状态发生变化的时候,所有依赖于它的对象(观察者)都会得到通知并自动更新。观察者模式有助于实现对象之间的解耦。

核心思想

弱化两个对象之间的耦合关系,观察者仅在主题中心事件改变时才参与活动。其目的就是使得关注游戏的一部分的所有代码集中到一起,降低各功能之间的耦合性。

实现原理

  • Subject(主题):主题是一个接口,定义用于添加,删除和通知观察者的方法。
  • ConcreteSubject(具体主题):实现了主题接口的类,其会维护一个观察者列表,并实现通知观察者的方法。
  • Observer(观察者):观察者是一个接口,定义用于更新观察者的方法
  • ConcreteObserver(具体观察者):实现了观察者接口的类,实现在被通知时更新自己的状态的方法。

适用场景

  • 当一个对象需要通知其他对象,但并不知道这些对象是谁时。比如:事件分发管理器,当用户进行一系列输入时(如键盘),将事件分发给添加了事件监听的对象。

unity 中的应用

比如角色收到伤害会有一些什么反应:例如增加 buff,暂时进入无敌,更新血量显示之类的事件。那么我们就可以将角色对象看作一个 Subject,定义具体发生事件效果为 Observer。在程序开始,就将其添加到 Subject 中角色受伤事件的观察者列表当中。角色受伤时,只需执行 Notify(TakeDamageEvent),即可广播给所有注册了这个事件的 Observer 对象。

缺点

  • 如果观察者和被观察者之间存在循环依赖,可能导致系统崩溃。
  • 通知观察者的顺序无法保证。