UF_OLDL2——缓存池模块

缓存池

为何要用缓存池

节选自:Unity —— 缓存池简单理解 - 知乎 (zhihu.com)

来理解缓存池之前,我们先了解一下C#的内存回收机制

每次实例化一个对象(在场景上创建一个对象),都会分配一个内存空间;
当这个对象被删除时,仅仅时断开了对这片空间的引用,此内存空间并没有被释放掉再次创建对象时,会继续分配其他的内存空间,直到内存被全部被分配满
当内存满了再回过头看有哪些是不用的"垃圾",再回收释放。

这样的一次释放,叫做 “一次GC”。
所谓垃圾,就是没有被任何变量、对象引用的内容
通过是否被引用来确定哪些对象是"垃圾"。

image

正是因为每次GC需要经过大量的计算来判断是否需要回收,对CPU的消耗较大,
所以每次GC都可能会造成卡顿,GC次数一旦多了会严重影响玩家的使用体验,由此出现了缓存池

在以前我们不使用缓存池时,例如子弹,我们一旦发射出去,打中什么了之后,就不再使用它,就会将其直接销毁
然而实际上,这些子弹对象仍然占用着内存空间,只是没有被引用
而这种对象的多次创建,内存空间被占满,这些没有被引用的对象内存空间将会触发GC而被回收释放,但是GC本身可能会造成卡顿!
随着游戏进行,如果不做应对,GC将会被频繁触发,进而引发卡顿,影响玩家体验

为了应对这种情况,我们就需要使用缓存池让类似于子弹这种一次性触发的对象可以在内存里被回收利用,减少GC

何为缓存池

打个比方,缓存池就像一个衣柜,把对象比作衣服,制作衣服比作实例化对象,销毁当作扔进垃圾桶,触发GC当作垃圾桶满了清理垃圾桶

刚开始时,这个衣柜没有任何衣服,
这时我们仍然需要制作衣服来获取需要的衣服,

是当这个衣服用完后,我们不再将其直接扔进垃圾桶,而是将其放入衣柜
当我们再需要这个衣服时,我们会先在衣柜里寻找有没有这个衣服
发现有,我们就直接穿衣柜里的衣服,而不是制作它

这样,我们解决了重复穿某件衣服时,需要重新制作衣服
以及垃圾桶被扔满了后要去清理垃圾的问题

当然,我们的衣服不可能只有一种类型(就像发射出去的子弹对象会有发射特效,子弹本体,击中特效)
所以,我们的衣柜需要一个个的抽屉,分门别类的放不同的衣服(对象按照不同的类,分门别类的集中缓存他们)

以上即缓存池的理论原理,接下来是代码实现

缓存池模块基础

知识点:字典,列表,GameObject和Resources的API

作用:当对象不需要时,直接传入缓存池,需要时在取出使用即可

使用方法:
当用完某对象时,使用PushObj()将其存储到缓存池并失活它,
而当需要使用某对象时,使用GetObj()方法获取并激活它

注意:传入的名字需要为该对象在Resource路径下的名字,才能保证实例化它,如果想要对象在被取出时做什么,请在OnEnable()内写逻辑

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 缓存池模块
/// </summary>
public class PoolManager : BaseManager<PoolManager>
{
/// <summary>
/// 缓存池容器,键为对象的类名,值为对应的游戏对象
/// </summary>
public Dictionary<string, List<GameObject>> poolDic = new Dictionary<string, List<GameObject>>();

/// <summary>
/// 通过Resource资源路径,向缓存池取东西
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <returns>取出的类对象</returns>
public GameObject GetObj(string name)
{
GameObject obj = null;
//缓存池有该对象类型的存储列表,且存储空间内有闲置的该类对象
if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
{
obj = poolDic[name][0];
poolDic[name].RemoveAt(0); //将对象从缓存池取出
}
//没有就从Resource文件夹里实例化一个
else
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name; //把对象名改为和存储池的名字(键名或者说存储路径)一样,便于动态的存储自己
}
obj.SetActive(true); //将缓存时失活的对象重新激活
return obj;
}

/// <summary>
/// 向缓存池存入暂时不用的东西,通过Resources资源路径来存储
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <param name="obj">要缓存的对象</param>
public void PushObj(string name, GameObject obj)
{
obj.SetActive(false); //缓存该对象之前,先失活这个对象
//当缓存池有该对象类型的存储列表时
if (poolDic.ContainsKey(name))
{
poolDic[name].Add(obj);
}
//没有就创建一个这种类型的存储列表
else
{
poolDic.Add(name, new List<GameObject>() { obj });
}
}
}

缓存池模块优化

上一个缓存池存在对象全部暴露在层级最外层的问题,这样会影响我们的Hierarchy窗口的观看,而且也不容易知道哪些对象是缓存池内的对象
以及,当切换场景时,场景上的对象都会被删除,这时缓存池存储对象就没有意义且会出错,应当清除存储池的所有对象

接下来的优化,会使我们的缓存池下使用若干个存储容器,分别存储不同类型的对象,
且在Hierarchy窗口下可以看到不同类型的对象依附在相应名字的空对象下
并且提供清空缓存池的方法

使用方法:
取出对象,传入该对象的Resources资源路径,即可获取到名字为传入的路径的对象
存入对象,填入该对象和对象的名字(直接填自己的名字即可),即可存储该对象
清空缓存池,直接Clear()即可,用于切换场景用

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 缓存池模块,一个缓存池模块下有若干个存储容器,分别存储不同类型的对象,按照对象名(Resources资源路径)划分类型
/// </summary>
public class PoolManager : BaseManager<PoolManager>
{
/// <summary>
/// 缓存池容器,键为对象名(Resources资源路径),值为对应的存储容器
/// </summary>
public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

/// <summary>
/// 缓存池下的所有存储容器都作为该对象的子物体
/// </summary>
private GameObject poolObj;

/// <summary>
/// 通过Resource资源路径,向缓存池取出东西
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <returns>取出的类对象</returns>
public GameObject GetObj(string name)
{
GameObject obj = null;
//缓存池有该对象类型的存储容器,且存储容器内有闲置的对象
if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
{
obj = poolDic[name].GetObj();
}
//不满足条件就根据传入Resource资源路径实例化一个这个对象
else
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name; //把对象名改为和存储池的名字(键名或者说存储路径)一样,便于动态的存储自己
}
return obj;
}

/// <summary>
/// 向缓存池存入暂时不用的东西,通过Resources资源路径来存储
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <param name="obj">要缓存的对象</param>
public void PushObj(string name, GameObject obj)
{
//如果不存在缓存池空物体对象,就先创建它,之后所有的存储容器都作为它的子对象
if (poolObj == null)
poolObj = new GameObject("Pool");

//当缓存池有该对象类型的存储容器时
if (poolDic.ContainsKey(name))
{
poolDic[name].PushObj(obj);
}
//没有就创建一个这种类型的存储列表
else
{
poolDic.Add(name, new PoolData(obj, poolObj));
}
}

/// <summary>
/// 清除缓存池的所有对象,以便于切换场景后对象池可以正常使用
/// </summary>
public void Clear()
{
poolDic.Clear();
poolObj = null;
}
}

/// <summary>
/// 缓存池下的某一种对象的存储容器,每一种类型(对象名)的对象都有一个自己的存储容器
/// </summary>
public class PoolData
{
/// <summary>
/// 缓存池内,相同类型(对象名)的对象所依附的父对象
/// </summary>
public GameObject fatherObj;

/// <summary>
/// 相同类型(对象名)的对象的存储列表
/// </summary>
public List<GameObject> poolList;

/// <summary>
/// 存储容器的构造函数,当不存在某种类型的存储容器时就使用它初始化,需要传入要存储的对象和需要依附的存储池对象
/// </summary>
/// <param name="obj">要存储的对象</param>
/// <param name="poolObj">所依附的缓存池对象</param>
public PoolData(GameObject obj, GameObject poolObj)
{
fatherObj = new GameObject(obj.name); //为该存储容器创建一个空对象,以后该存储容器内的对象都会依附于该父对象下面
fatherObj.transform.parent = poolObj.transform; //将空对象依附于缓存池对象
poolList = new List<GameObject>(); //初始化存储列表
PushObj(obj); //把要存储的对象存进去
}

/// <summary>
/// 向存储容器存储对象,并将其作为该存储容器的对象下的子对象
/// </summary>
/// <param name="obj"></param>
public void PushObj(GameObject obj)
{
obj.SetActive(false); //将存入缓存池的对象失活
poolList.Add(obj); //将对象存入存储列表内
obj.transform.parent = fatherObj.transform; //将对象设置为该存储列表的子对象
}

/// <summary>
/// 从存储容器取出对象
/// </summary>
/// <returns>要取出的对象</returns>
public GameObject GetObj()
{
GameObject obj = null;
obj = poolList[0];
poolList.RemoveAt(0); //将对象从存储列表内取出
obj.transform.parent = null; //将取出的对象切断其与缓存池空对象的父子关系
obj.SetActive(true); //将缓存时失活的对象重新激活
return obj;
}
}

运用资源异步加载的缓存池模块优化

通过资源加载模块异步加载的方法,我们可以优化在缓存池不存在某资源时实例化资源,通过异步加载来提升性能

使用方法:
相比上面的缓存池模块,这次的优化重载了一个获取资源的方法,它使用加载资源模块异步加载资源
传入的参数变为:传入该对象的Resources资源路径,有参数的回调函数,
回调函数在加载完成后执行,执行时参数为读取到的游戏对象

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 缓存池模块,一个缓存池模块下有若干个存储容器,分别存储不同类型的对象,按照对象名(Resources资源路径)划分类型
/// </summary>
public class PoolManager : BaseManager<PoolManager>
{
/// <summary>
/// 缓存池容器,键为对象名(Resources资源路径),值为对应的存储容器
/// </summary>
public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

/// <summary>
/// 缓存池下的所有存储容器都作为该对象的子物体
/// </summary>
private GameObject poolObj;

/// <summary>
/// 通过Resource资源路径,向缓存池取出东西,不存在就直接使用Resources同步加载实例化它
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <returns>取出的类对象</returns>
public GameObject GetObj(string name)
{
GameObject obj = null;
//缓存池有该对象类型的存储容器,且存储容器内有闲置的对象
if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
{
obj = poolDic[name].GetObj();
}
//不满足条件就根据传入Resource资源路径实例化一个这个对象
else
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name; //把对象名改为和存储池的名字(键名或者说存储路径)一样,便于动态的存储自己
}
return obj;
}

/// <summary>
/// 通过Resource资源路径,向缓存池取出东西,通过传入的回调函数参数获取资源,不存在就异步加载它
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <param name="callback">资源获取完成后执行的,通过参数获取资源的回调函数</param>
public void GetObj(string name, UnityAction<GameObject> callback)
{
//缓存池有该对象类型的存储容器,且存储容器内有闲置的对象
if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
{
callback(poolDic[name].GetObj());
}
//不满足条件就根据传入Resource资源路径实例化一个这个对象
else
{
ResourcesManager.Instance().LoadAsync<GameObject>(name, (o) =>
{
o.name = name;
callback(o);
});
}
}

/// <summary>
/// 向缓存池存入暂时不用的东西,通过Resources资源路径来存储
/// </summary>
/// <param name="name">Resources资源路径(对象名)</param>
/// <param name="obj">要缓存的对象</param>
public void PushObj(string name, GameObject obj)
{
//如果不存在缓存池空物体对象,就先创建它,之后所有的存储容器都作为它的子对象
if (poolObj == null)
poolObj = new GameObject("Pool");

//当缓存池有该对象类型的存储容器时
if (poolDic.ContainsKey(name))
{
poolDic[name].PushObj(obj);
}
//没有就创建一个这种类型的存储列表
else
{
poolDic.Add(name, new PoolData(obj, poolObj));
}
}

/// <summary>
/// 清除缓存池的所有对象,以便于切换场景后对象池可以正常使用
/// </summary>
public void Clear()
{
poolDic.Clear();
poolObj = null;
}
}

/// <summary>
/// 缓存池下的某一种对象的存储容器,每一种类型(对象名)的对象都有一个自己的存储容器
/// </summary>
public class PoolData
{
/// <summary>
/// 缓存池内,相同类型(对象名)的对象所依附的父对象
/// </summary>
public GameObject fatherObj;

/// <summary>
/// 相同类型(对象名)的对象的存储列表
/// </summary>
public List<GameObject> poolList;

/// <summary>
/// 存储容器的构造函数,当不存在某种类型的存储容器时就使用它初始化,需要传入要存储的对象和需要依附的存储池对象
/// </summary>
/// <param name="obj">要存储的对象</param>
/// <param name="poolObj">所依附的缓存池对象</param>
public PoolData(GameObject obj, GameObject poolObj)
{
fatherObj = new GameObject(obj.name); //为该存储容器创建一个空对象,以后该存储容器内的对象都会依附于该父对象下面
fatherObj.transform.parent = poolObj.transform; //将空对象依附于缓存池对象
poolList = new List<GameObject>(); //初始化存储列表
PushObj(obj); //把要存储的对象存进去
}

/// <summary>
/// 向存储容器存储对象,并将其作为该存储容器的对象下的子对象
/// </summary>
/// <param name="obj"></param>
public void PushObj(GameObject obj)
{
obj.SetActive(false); //将存入缓存池的对象失活
poolList.Add(obj); //将对象存入存储列表内
obj.transform.parent = fatherObj.transform; //将对象设置为该存储列表的子对象
}

/// <summary>
/// 从存储容器取出对象
/// </summary>
/// <returns>要取出的对象</returns>
public GameObject GetObj()
{
GameObject obj = null;
obj = poolList[0];
poolList.RemoveAt(0); //将对象从存储列表内取出
obj.transform.parent = null; //将取出的对象切断其与缓存池空对象的父子关系
obj.SetActive(true); //将缓存时失活的对象重新激活
return obj;
}
}