单例的特性就是有风险的同时带来了便利

单例的特性

单例有两个特性:

  • 保证一个类仅有一个实例。
  • 提供一个全局访问的访问点(Instance)

从特性我们可以总结出:

  • 单例有生命周期,有状态(内部的数据),因为单例是对象实例。
  • 单例可以在全局的任何一个地方获取。

风险也有两点:

  • 如果是懒加载(第一次访问时创建实例)的单例,则看到单例调用时,不知道哪里做了初始化。
  • 任意位置可以访问,项目规模一旦增大就会容易造成混乱。

全局访问的风险

生命周期不确定的风险与全局访问的风险相比,后者造成的危害是慢性的,也是相对严重的。

我们来展示单例的一般使用:

1
2
3
4
5
6
7
8
9
10
namespace FrameworkDesign2021.BestSingleton
{
public class GameManager : MonoSingleton<GameManager>
{
public void Play() {}
public void Pause() {}
public void Resume() {}
public void GameOver() {}
}
}

以上代码,就是游戏管理器,实现了一些方法。

使用的代码如下:

1
2
GameManager.Instance.Play();
GameManager.Instance.Pause();

这样使用是常见的使用方式,但是风险在哪里呢?

风险在GameManager.Instance中的Instance属性。

在使用中,我们拿到了完整的GameManager对象。那么关于这个对现象的一切,我们都是有办法获取的。

我们再看一下以下的代码

1
2
3
GameManager.Instance.Score += 5;
GameManager.Instance.Health += 10;
GameManager.Instance.Play();

Instance暴露太多了,而且以上的三行代码非常可能是顺序相关的。

例如,我们调整代码的使用顺序,如下:

1
2
3
GameManager.Instance.Score += 5;
GameManager.Instance.Play();
GameManager.Instance.Health += 10;

这段代码的结果可能与之前的结果不一样。

我们需要尽量控制API的暴露,不可以全部暴露出来,甚至Instance这个东西,笔者也不希望暴露出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace FrameworkDesign2021.BestSingleton
{
public class GameManager : MonoBehaviour,ISingleton
{
private static GameManager mInstance//private
{
get { return MonoSingletonProperty<GameManager>.Instance; }
}

void ISingleton.OnSingletonInit() {}

private int mScore = 0;
private int mHealth = 0;

public static void Play(int addScore,int addHealth)
{
mInstance.mScore += addScore;
mInstance.mHealth += addHealth;
//do something
}
public static void Pause() {}
public static void Resume() {}
public static void GameOver() {}
}
}

GameManager发生了一下改变:

  1. 单例的实现使用MonoSingletonProperty实现的,并且是private访问权限。
  2. Score、Health改成私有的权限了。
  3. 对外开放的API都是静态的,比如Play、Pause、Resume、GameOver
  4. Play增加了两个参数

使用方式如下:

1
GameManager.Play(5,5);//三句合一

这样就可以避免,调用顺序不对的问题了,而且我们非常严格地控制了API的暴露,从而减缓了全局访问的风险。

当前的GameManager结构如下。

GameManager(Instance)

Instance ——> Play

​ ——>Pause

​ ——>Resume

这就是单例的最佳实践,实际上QFramework的UIKit则使用的就是此结构

1
2
UIKit.OpenPanel<UIMainPanel>();
UIKit.ClosePanel<UIMainPanel>();

生命周期不稳定的风险

懒加载会在第一次访问的时候创建实例。此时会出现一些问题。

比如同一帧做了大量的资源加载操作(在单例的初始化时),造成大量的GC。

避免这种问题的办法非常简单,就是控制单例的初始化时机。比如,在GameManager类中加上Init方法

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace FrameworkDesign2021.BestSingleton
{
public class GameManager : MonoBehaviour,ISingleton
{
//...
public static void Init()
{
mInstance.mScore = 0;
mInstance.mHealth = 0;
}
//...
}
}

这样我们就可以在合适的时机,主动去初始化这个单例。比如进入游戏场景前,或者项目初始化时,代码如下:

1
GameManager.Init();

一定要记住这个SingletonKit内的单例都是懒加载(不管是普通单例还是Mono单例),也就是Mono单例的创建不依赖Awake和Start这种生命周期函数,所以要用这种方式。

小结

关于单例的使用:

  1. 严格控制对外暴露的API
  2. 严格控制单例的使用范围(下一篇介绍)
  3. 严格控制单例对象的生命周期(创建、初始化、销毁)