UFL1-3——安全性问题解决
UFL1-3——安全性问题解决
前置知识点
细节知识点
- C#中抽象类的知识点(C#四部曲之C#核心)
- C#中反射相关知识点(C#四部曲之C#进阶)
- Unity中Destroy函数(Unity四部曲Unity入门)
- Unity中特性相关知识(Unity四部曲Unity入门)
唯一性问题——构造函数问题的解决
虽然公共构造函数可能给我们带来破坏唯一性的安全隐患
但是在实际开发中,单例模式是非常基础的知识点,可以说是程序员的必备基础知识
在使用时几乎不会有人马虎到自己去new
单例模式对象
因此即使我们不做这些安全性的处理,其实也不会有太大问题,根据实际情况和需求进行设计即可
构造函数带来的唯一性问题
- 对于不继承
MonoBehaviour
的单例模式基类:我们要避免在外部 new
单例模式类对象 - 对于继承
MonoBehaviour
的单例模式基类:由于继承MonoBehaviour
的脚本不能通过new
创建,因此不用过多考虑
类似于下面的代码就会出现安全性问题,我们在外部new
了一个单例模式的类对象,这破坏了单例模式唯一性:
1 | TestMgr t = new TestMgr(); |
解决构造函数带来的问题
-
父类变为抽象类
-
规定继承单例模式基类的类必须显示实现私有无参构造函数
-
在基类中通过反射来调用私有构造函数实例化对象
主要知识点:
利用Type
中的GetConstructor(约束条件, 绑定对象, 参数类型, 参数修饰符)
方法
来获取私有无参构造函数1
2
3
4
5ConstructorInfo constructor = typeof(T).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic, //表示成员私有方法
null, //表示没有绑定对象
Type.EmptyTypes, //表示没有参数
null); //表示没有参数修饰符
修改后的单例模式基类如下:
1 | /// <summary> |
当然,也可以使用Activator.CreateInstance
方法来实例化:
1 | public static T Instance |
最后,如果想要避免他人使用反射来实例化对象,需要一个变量去标识是否实例化,然后在构造函数里检测该标识
一个简单的方式是,在单例模式基类里声明一个protected
的bool
类型属性isInitialized
,
这个属性通过instance
是否为空来决定是否为true
,如果instance
为空就是true
,
然后在其派生类的构造函数里检测这个isInitialized
是否为true
,如果为true
,则抛出错误,代码如下:
基类的代码(实测ConstructorInfo
或者Activator.CreateInstance
方法都可正常使用):
1 | public abstract class BaseManager<T> where T : class |
派生类的代码:
1 | using UnityEngine; |
美中不足的一点是,无法防止单例模式的类在第一次调用Instance
前,外部调用反射构造方法实例化对象的情况,因此需要更进一步:
在基类声明一个私有的bool
类型标识,默认为false
,这个标识只有在Instance
内实例化对象时赋值true
,
实例化结束后赋值为false
,根据该标识是否为false
决定构造函数内是否抛出错误:
基类的代码是:
1 | /// <summary> |
测试代码为:
1 | using System; |
唯一性问题——重复挂载
为了避免重复挂载我们一般采用以下几种方案:
- 对于挂载式的单例模式基类,相同对象上重复挂载问题,通过添加
[DisallowMultipleComponent]
特性解决 - 对于挂载式的单例模式基类,不同对象上的重复挂载,通过逻辑判断,代码移除多余的脚本
- 最好的避免重复挂载的方式,就是使用自动挂载式的单例模式基类,并且制定使用规则(不允许手动挂载和代码添加)
对于继承MonoBehaviour
的挂载式的单例模式基类
-
手动挂载多个相同单例模式脚本
-
代码动态添加多个相同单例模式脚本
1
this.gameObject.AddComponent<>()
这些行为都会破坏单例模式的唯一性
-
对于挂载式的单例模式脚本
-
同个对象的重复挂载
为脚本添加特性
[DisallowMultipleComponent]
该特性只能使脚本不能在一个对象上重复挂载,但是不能解决在不同对象上挂多个脚本的问题 -
修改代码逻辑
判断如果存在对象,移除脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get { return instance; }
}
/// <summary>
/// 在重写该Awake函数时切记保留base.Awake()!!!
/// </summary>
protected virtual void Awake()
{
//已经存在一个对应的单例模式对象了,不需要再有一个
if (instance != null)
{
Destroy(this);
return;
}
instance = this as T;
DontDestroyOnLoad(this.gameObject);
}
}
-
-
对于自动挂载式的单例模式脚本,制定使用规则,不允许手动挂载或代码添加
线程安全——是否加锁
- 对于不继承
MonoBehaviour
的单例模式对象,建议加锁 - 对于继承
MonoBehaviour
的单例模式对象,可以不加
如果程序当中存在多线程,我们需要考虑当多个线程同时访问同一个内存空间时出现的问题
如果不加以控制,可能会导致数据出错,我们一般称这种问题为多线程并发问题,指多线程对共享数据的并发访问和操作。
而一般解决该问题的方式,就是通过C#中的lock
关键字进行加锁,我们需要考虑我们的单例模式对象们是否需要加锁(lock)
lock
的原理保证了在任何时刻只有一个线程能够执行被锁保护的代码块
从而防止多个线程同时访问或修改共享资源,确保线程安全
但是具体是否加锁,都根据需求来定,如果你的项目中压根就不会使用多线程,那么完全可以不用考虑加锁问题
-
不继承
MonoBehaviour
的单例模式建议加锁,避免以后使用多线程时出现并发问题, 比如在处理网络通讯模块、复杂算法模块时,经常会进行多线程并发处理
例如多线程访问
Instance
时,需要避免instance
单例对象被多个线程同时访问Instance
导致重复实例化
因此需要为实例化instance
的代码块加锁,为了性能考虑,判断instance == null
是不需要放入锁中的但是如果多个线程同时进入了外层
instance == null
的代码块,线程先后进入带锁代码块,还是会出现重复实例化instance
的情况
因此锁内必须再添加判断instance == null
,确保最先进入锁内的线程在实例化instance
后,后进入带锁代码块的线程将不能再实例化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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/// <summary>
/// 不继承MonoBehaviour的单例模式基类,实现静态变量和静态属性的声明,单例模式类可以直接继承该类,继承后无需自行实现单例声明相关内容
/// </summary>
/// <typeparam name="T">继承该类的类</typeparam>
public abstract class BaseManager<T> where T : class
{
private static T instance;
private static bool internalInitializeToken = false; //类内部实例化标识,只有Intance内才可以修改它,用于确保内部可以实例化对象
protected static readonly object lockObj = new object(); //用于加锁的对象
//当instance不为null时且内部标识是false时,则表示不能实例化
protected static bool CanInitialize => instance == null && internalInitializeToken;
public static T Instance
{
get
{
if (instance == null)
{
lock (lockObj)
{
//这里的判空不可以删除!它确保最先进入锁内的线程在实例化结束后,后进入带锁代码块的线程不能再实例化instance
if (instance == null)
{
Type type = typeof(T);
//使用反射来获取私有无参构造函数,并用于对象的实例化
//在这里实例化前先让该标识为true
internalInitializeToken = true;
instance = Activator.CreateInstance(type, true) as T;
if (instance == null)
Debug.LogError("无法实例化对象!");
//实例化后恢复为false
internalInitializeToken = false;
}
}
}
return instance;
}
}
} -
继承
MonoBehaviour
的单例模式可加可不加,但是建议不加
因为Unity中的机制是,Unity主线程中处理的一些对象(如GameObject
、Transform
等等)是不允许被其他多线程修改访问的,会直接报错
因此我们一般不会通过多线程去访问继承MonoBehaviour
的相关对象,既然如此,就不会发生多线程并发问题