不少开发者或者有经验的老手都会建议尽可能不要用单例模式,这是有缘由的。git
单例模式是设计模式中最简单的也是你们一般最早接触的一种设计模式。在框架的设计中一些管理类或者系统类多多少少都会用到单例模式,好比 QFramework 中的 UIMgr,ResMgr 都是单例。固然在平时的游戏开发过程当中也会用到单例模式,好比数据管理类,角色管理类等等,以上这些都是很是常见的使用单例的应用场景。github
那么今天笔者想好好聊聊单例的使用上要注意的问题,但愿你们对单例有更立体的认识,并介绍 QFramework 中单例套件的使用和实现细节。设计模式
本篇文章分为四个主要内容:缓存
可能说有的朋友不太了解单例,笔者先对单例作一个简单的介绍。微信
保证一个类仅有一个实例,并提供一个访问它的全局访问点。架构
定义比较简洁并且不难理解。框架
再引用一个比较有意思的例子函数
俺有6个漂亮的老婆,她们的老公都是我,我就是咱们家里的老公 Singleton,她们只要说道“老公”,都是指的同一我的,那就是我(刚才作了个梦啦,哪有这么好的事)。-《泡妞与设计模式》工具
这个例子很是形象地介绍了咱们平常开发中使用单例类的情景,无论在哪里均可以得到同一个而且惟一的单例类的实例。性能
关于单例模式的简介就到这里,实现的细节和对模式更详尽的介绍网上处处都是,这里再也不浪费篇幅。
上一篇文章中说到的 Manager of Managers 架构,其中每一个 Manager 在 QFramework 中都是由单例实现,固然也可使用静态类实现,可是相比于静态类的实现,单例更为合适。
先分析下需求,当设计一个 Manager 时候,咱们但愿整个程序只有一个该 Manager 类的实例,通常立刻能想到的实现是这样的:
public class XXXManager { private static XXXManager instance = null; private XXXManager { // to do ... } public static XXXManager() { if (instance == null) { instance = new XXXManager(); } return instance; } }
若是一个游戏须要10个各类各样的 manager,那么以上这些代码要复制粘贴好多遍。重复的代码太多!!! 想要把重复的代码抽离出来,怎么办?
答案是引入泛型。
实现以下:
namespace QFramework { public abstract class Singleton<T> where T : Singleton<T> { protected static T mInstance = null; protected Singleton() { } public static T Instance { get { if (mInstance == null) { // 如何 new 一个T??? } return mInstance; } } } }
为了能够被继承,静态实例和构造方法都使用了 protect 修饰符。以上的问题很显而易见,那就是不能 new 一个泛型(2016 年 3月9日补充:并非不能new一个泛型,参考:new 一个泛型的实例,编译失败了,为何?-CSDN论坛-CSDN.NET-中国最大的IT技术社区),(2016 年 4月5日补充:有同窗说能够new一个泛型的实例,不过要求改泛型提供了 public 的构造函数,好吧,这里不用new的缘由是,没法显示调用 private 的构造函数)。由于泛型自己不是一个类型,那该怎么办呢?答案是使用反射。
这部分之后可能会复用,因此抽出了 SingletonCreator.cs,专门用来经过反射建立私有构造示例。
实现以下:
SingletonCreator.cs
namespace QFramework { using System; using System.Reflection; public static class SingletonCreator { public static T CreateSingleton<T>() where T : class, ISingleton { // 获取私有构造函数 var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); // 获取无参构造函数 var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0); if (ctor == null) { throw new Exception("Non-Public Constructor() not found! in " + typeof(T)); } // 经过构造函数,常见实例 var retInstance = ctor.Invoke(null) as T; retInstance.OnSingletonInit(); return retInstance; } } }
但愿在单例类的内部得到初始化事件因此定制了 ISingleton 接口用来接收单例初始化事件。
ISingleton.cs
namespace QFramework { public interface ISingleton { void OnSingletonInit(); } }
Singleton.cs
namespace QFramework { public abstract class Singleton<T> : ISingleton where T : Singleton<T> { protected static T mInstance; static object mLock = new object(); protected Singleton() { } public static T Instance { get { lock (mLock) { if (mInstance == null) { mInstance = SingletonCreator.CreateSingleton<T>(); } } return mInstance; } } public virtual void Dispose() { mInstance = null; } public virtual void OnSingletonInit() { } } }
以上就是最终实现了,而且加上了线程锁,并且实现了一个用来接收初始化事件的接口 ISingleton。这个实现是在任何 C# 程序中都是通用的。其测试用例以下所示:
using QFramework; // 1.须要继承 Singleton。 // 2.须要实现非 public 的构造方法。 public class XXXManager : Singleton<XXXManager> { private XXXManager() { // to do ... } } public static void main(string[] args) { XXXManager.Instance.xxxyyyzzz(); }
这个单例的模板是平时用得比较顺手的工具了,其实现是在其余的框架中发现的,拿来直接用了。反射的部分可能会耗一些性能,可是第一次调用只会执行一次,因此放心。在 Unity 中可能会须要继承 MonoBehaviour 的单例,由于不少游戏可能会只建立一个 GameObject,用来获取 MonoBehaviour 的生命周期,这些内容会再下一节中介绍:)。
上一小节讲述了如何设计 C# 单例的模板。也随之抛出了问题:
先分析下需求:
首先,第一点,约束脚本实例对象的个数,这个在上一篇中已经实现了。 可是第二点,约束 GameObject 的个数,这个需求,尚未思路,只好在游戏运行时判断有多少个 GameObject 已经挂上了该脚本,而后若是个数大于1抛出错误便可。 第三点,经过继承 MonoBehaviour 实现,只要覆写相应的回调方法便可。 第四点,在脚本销毁时,把静态实例置空。 完整的代码就以下所示:
using UnityEngine; /// <summary> /// 须要使用Unity生命周期的单例模式 /// </summary> namespace QFramework { public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T> { protected static T mInstance = null; public static T Instance() { if (mInstance == null) { mInstance = FindObjectOfType<T>(); if (FindObjectsOfType<T>().Length > 1) { Debug.LogError("More than 1!"); return instance; } if (instance == null) { string instanceName = typeof(T).Name; Debug.Log ("Instance Name: " + instanceName); GameObject instanceGO = GameObject.Find(instanceName); if (instanceGO == null) instanceGO = new GameObject(instanceName); instance = instanceGO.AddComponent<T>(); DontDestroyOnLoad(instanceGO); //保证明例不会被释放 Debug.Log ("Add New Singleton " + mInstance.name + " in Game!"); } else { Debug.Log("Already exist: " + mInstance.name); } } return mInstance; } protected virtual void OnDestroy() { mInstance = null; } } }
这样一个独立的 MonoSingleton 就实现了。
目前已经实现了两种单例的模板,一种是须要接收 MonoBehaviour 生命周期的,一种是不须要接收生命周期的 C# 单例的模板,能够配合着使用。虽然不是本人实现的,可是用起来但是超级爽快,2333。
文章写到这,咱们已经实现了 C# 单例的模板和 MonoBehaviour 单例的模板,这两个模板已经能够知足大多数实现单例的需求了。可是偶尔仍是会遇到比较奇葩的需求的。
好比这样的需求:
虽然单例继承其余类是比较脏的设计,可是不免会遇到不得不继承的时候。没有最好的设计,只有最合适的设计。
解决方案:
XXX.Instance.ABCFunc();
以前的单例的模板代码以下所示:
namespace QFramework { public abstract class Singleton<T> : ISingleton where T : Singleton<T> { protected static T mInstance; static object mLock = new object(); protected Singleton() { } public static T Instance { get { lock (mLock) { if (mInstance == null) { mInstance = SingletonCreator.CreateSingleton<T>(); } } return mInstance; } } public virtual void Dispose() { mInstance = null; } public virtual void OnSingletonInit() { } } }
按照之前的方式,若是想实现一个单例的代码应该是这样的:
using QFramework; // 1.须要继承QSingleton。 // 2.须要实现非public的构造方法。 public class XXXManager : QSingleton<XXXManager> { private XXXManager() { // to do ... } } public static void main(string[] args) { XXXManager.Instance().xxxyyyzzz(); }
若是我想 XXXManager 继承一个 BaseManager 代码就变成这样了
using QFramework; // 1.须要继承QSingleton。 // 2.须要实现非public的构造方法。 public class XXXManager : BaseManager { private XXXManager() { // to do ... } }
这样这个类就不是单例了,怎么办?
答案是经过 C# 的属性器。
using QFramework; // 1.须要继承QSingleton。 // 2.须要实现非public的构造方法。 public class XXXManager : BaseManager,ISingleton { private XXXManager() { // 不建议在这里初始化代码 } void ISingleton.OnSingletonInit() { // to do ... } public static XXXManager Instance { get { return SingletonProperty<XXXManager>.Instance; } } } public static void main(string[] args) { XXXManager.Instance.xxxyyyzzz(); }
好了,又看到陌生的东西了,SingletonProperty 是什么?
和以前的单例的模板很类似,贴上代码本身品吧...
namespace QFramework { public static class SingletonProperty<T> where T : class, ISingleton { private static T mInstance; private static readonly object mLock = new object(); public static T Instance { get { lock (mLock) { if (mInstance == null) { mInstance = SingletonCreator.CreateSingleton<T>(); } } return mInstance; } } public static void Dispose() { mInstance = null; } } }
这样没法继承的问题就解决啦。
缺点是:相比于 Singleton,SingletonProperty 在使用时候多了一次函数调用,并且还要再实现个 getter,不过问题解决啦,。
在介绍单例的最佳实践以前,咱们要先分析下单例的利弊。
首先咱们先从定义上入手。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
就两句话:
保证一个类仅有一个实例,这个是对单例的一个需求。可是这句话没有告诉你,这个实例何时应该去建立。而笔者所知到的建立方式通常是有两种,第一种是在程序编译后立刻建立,通常实现方式是在声明静态成员变量的时候去 new 一个实例,实现以下。
public class Test { public static readonly Test Instance = new Test(); }
这种方式最简单,也最容易实现。
第二种则第一次获取实例时去建立,实现以下:
public class Test { public static Test mInstance; public static Test Instance { get { if (mInstance == null) { mInstance = new Test(); } return mInstance; } } }
这种单例实现也比较常见,被称为懒单例模式,乘坐懒的缘由是用到的时候再去建立,这样能够减缓内存和 CPU 压力。形成的风险则是,声明周期不可控。
因此说第一个利弊是懒加载的利弊。
懒加载是可用不可用的,在 Unity 开发中通常用单例的模板时候都是用懒加载的方式的。
其余的还有 全局惟一 和 全局访问。
全局惟一这个没什么好说的,单例的存在就是为了保证全局惟一,只有个优势吧。
提供全局访问。提供全局访问这个功能,优势是方便获取单例实例。缺点就很明显了,在文章的开始,笔者说
不少开发者或者有经验的老手都会建议尽可能不要用单例模式,这是有缘由的。
这个缘由就是由于全局访问。一个实例的全局访问会有不少风险,固然静态类也是能够全局访问的。可是静态类通常咱们用做工具或者 Helper,因此没什么问题。可是单例自己是一个实例,是一个对象。因此对象有的时候是有声明周期的,而且有时候还有上下文(缓存的数据、状态)。而有时候还须有必定特定的顺序去使用 API。这些都是很是有可能的。 因此说要设计一个好的单例类,好的管理类。是对开发者要求是很是高的。不过在这里笔者提醒一下,不是说要把单例类设计得很是好才是彻底正确的。有的时候,咱们来不及花精力去设计,考虑周全,可是能够完成工做,完成任务,这样最起码是对得起公司付的工资的,并且功能完成了,等不忙的时候能够回来再思考的嘛,罗马不是一天建成的,可是罗马能够经过一点一点迭代完成。具体要求高在哪里,主要是符合设计模式的六大设计原则就好。
接下来笔者就贴出一个笔者认为比较严格的单例类设计。
原则上是,保留单例优势的同时,去削弱使用它的风险。
目前来看,单例使用的风险主要是全局访问,因此削弱全局访问就行了。笔者所分享的方式是,对外提供的 API 都用静态 API。Instance 变量不对外提供,外部访问只能经过静态的 API。而内部则维护一个私有的单例实例。
代码以下:
using System; using QFramework; using UnityEngine; /// <summary> /// 职责: /// 1. 用户数据管理 /// 2. 玩家数据管理 /// 3. Manager 容器: List/Dictionary 增删改查 /// </summary> /// /// public class PlayerData { public string Username; public int Level; public string Carrer; } [QMonoSingletonPath("[Game]/PlayerDataMgr")] public class PlayerDataMgr : MonoBehaviour,ISingleton { private static PlayerDataMgr mInstance { get { return MonoSingletonProperty<PlayerDataMgr>.Instance; } } /// <summary> /// 对外阉割 /// </summary> void ISingleton.OnSingletonInit() { mPlayerData = new PlayerData(); // 从本地加载的一个操做 } #region public 对外提供的 API public static void SavePlayerData() { mInstance.Save(); } public static PlayerData GetPlayerData() { return mInstance.mPlayerData; } #endregion private PlayerData mPlayerData; private void Save() { // 保存到本地 } }
使用上很是干净简洁:
public class TestMonoSingletonA : MonoBehaviour { // Use this for initialization private void Start() { var playerData = PlayerDataMgr.GetPlayerData(); playerData.Level++; PlayerDataMgr.SavePlayerData(); } // Update is called once per frame void Update () { } }
到这里还要补充一下,笔者呢不太喜欢 Instance 这个命名。在命名上,不少书籍都建议用业务命名而不是用技术概念来命名。
好比 PlayerDataSaver 是业务命名,可是 SerializeHelper 则是技术命名,本质上他们两个均可以作数据存储相关的任务。可是 PlayerDataSaver,更适合人类阅读。
Instance 是技术概念命名,而不是业务命名。尽可能不要让技术概念的命名出如今 UI/逻辑层。只能够在框架层或者插件曾出现是容许的。
以上这些是笔者的本身的观点,不是标准的原则,你们看看就好。
今天的内容就这些,谢谢阅读~
个人框架地址:https://github.com/liangxiegame/QFramework
教程源码:https://github.com/liangxiegame/QFramework/tree/master/Assets/HowToWriteUnityGameFramework/
QFramework&游戏框架搭建QQ交流群: 623597263
转载请注明地址:凉鞋的笔记http://liangxiegame.com/
微信公众号:liangxiegame
若是以为本篇教程或者 QFramework 对您有帮助,不妨经过如下方式赞助笔者一下,鼓励笔者继续写出更多高质量的教程,也让更多的力量加入 QFramework 。
笔者在这里保证 QFramework、入门教程、文档和此框架搭建系列的专栏永远免费开源。以上捐助产品的内容对于使用 QFramework 的使用来说都不是必须的,因此你们不用担忧,各位使用 QFramework 或者 阅读此专栏 已是对笔者团队最大的支持了。