Unity中的C#内存管理(一)
来源:互联网 发布:淘宝详情页添加视频 编辑:程序博客网 时间:2024/05/22 01:31
本系列文章的第一部分讨论了.Net/Mono和unity内存管理的基本知识。第二部分深入介绍了Unity Profiler和CIL相关知识,以发现C#代码中不必要的内存分配。
第三篇文章即本文将介绍对象池。到目前为止,我们一直关注的是堆分配。现在我们还想要避免不必要的内存释放,以至于在游戏运行时不会因为垃圾回收器(GC)回收内存而产生帧率下降。对象池是解决这个问题的理想方案。我将展示三种对象池的完整代码(你可以在GithubGist中找到这些代码)。
从一个非常简单的对象池类开始
对象池背后的理念非常简单。不再使用new创建新的对象,而是在对象池中储存用过的对象,允许他们之后再被回收,从而在需要时重复使用他们。对象池最重要的一个特性、真正的对象池设计模式的本质是当我们需要获得一个新的对象时,不需要关心对象是重新创建的还是循环使用的原来对象。下面几行代码就是对象池设计模式的具体实现:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
public
class
ObjectPool<T> where T :
class
,
new
()
{
private
Stack<T> m_objectStack =
new
Stack<T>();
public
T New()
{
return
(m_objectStack.Count == 0) ?
new
T() : m_objectStack.Pop();
}
public
void
Store(T t)
{
m_objectStack.Push(t);
}
}
非常简单,但这的确是核心模式的完美实现。(如果你不懂"where T..."语法,下面将会解释)。想用这个类,就不能像下面这样用new操作符来创建类:
[C#] 纯文本查看 复制代码
1
2
void
Update() {
MyClass m =
new
MyClass();
}
而是成对使用 New() 和 Store()方法:
[C#] 纯文本查看 复制代码
1
2
3
4
5
6
ObjectPool<MyClass> poolOfMyClass =
new
ObjectPool<MyClass>();
void
Update()
{
MyClass m = poolOfMyClass.New();
// do stuff...
poolOfMyClass.Store(m);
}
这样比较繁琐,因为你需要记住在New()方法之后在正确的位置调用Store()方法。不幸的是,没有一种通用的方法来简化此设计模式的使用,因为不管是ObjectPool还是C#编译器都不知道对象什么时候可以被重新使用。恩,其实还有一种通过垃圾回收器自动管理内存的方法。这种方法的缺点在文章开始处你已经读过。也就是说,在幸运的情况下,你可以使用文章最后说明的“对象池全部重置”模式。那里,所有的Store()调用都会被替换为调用ResetAll()方法。
增加ObjectPool 类的复杂度
我是简洁代码(simplicity)的粉丝,大道至简。但是,ObjectPool 类现阶段可能有些太简单了。如果你搜索C#的对象池库,会找到很多解决方案,其中有些方案相当精妙且复杂。因此,退一步来思考我们需要或者不需要什么功能,比如通用的对象池查找功能。
- 许多对象类型在被重新使用之前需要以某种方式“重置”。至少,所有的成员变量都可以被设置为其默认状态。这些都是由对象池透明处理而非使用者。重置调用的时机和方式由下面两个设计特征决定:
积极重置(每次存储时重置)或者延迟重置(对象使用前重置)。
重置由池(由池处理,对类来说透明)或者类(对池对象的声明者透明)来管理。
- 在上面的例子中,对象池“poolOfMyClass”被显示声明为类成员变量。很明显,这样的对象池需要为每个新类型资源声明一个实例(My2ndClass等)。还有一种方案,ObjectPool类可以创建和管理这些对象池,而用户不用关心这些。
- 你在这里、还有这里可以找到几个对象池库,用它们来管理各种类型的资源(内存、数据库连接、游戏对象、外部assets等)。这往往会增加对象池代码的复杂度,因为,不同资源的处理逻辑差别很大。
- 一些稀缺资源类型(例如,数据库连接)对象池需要强制设定使用上限,并提供一种安全的方式来分配一个新的或者循环使用的对象。
- 如果对象池在某些时刻创建了大量对象,我们可能希望对象池有能力减少对象的创建(自动或者按命令)。
- 最后,对象池可以由多个线程共享,在这种情况下它必须是线程安全的。
上面这些特性哪些是值得实现的呢?我们都有自己的看法。但是,我来解释一下我自己的优先级。
- 重置功能必须有。但是,下面你会发现,完全没必要纠结重置逻辑是由对象池还是托管类来处理。你可能两者都需要,后文的代码分别实现了两种情况。
- Unity强加了多线程限制。基本上,除了游戏主线程之外你还可以创建一个工作线程。但是,只有游戏主线程可以调用Unity的API。在我看来,这意味着我们可以为所有线程单独创建对象池,因此也可以移除“多线程支持”的需求。
- 就个人而言,我不介意为每个对象类型声明一个新的对象池。但是,还有一种方案:单例模式。ObjectPool类通过存储在静态变量中的对象池Dictionary创建和保存对象池实例。要让其正常工作,你必须保证ObjectPool类可以在多线程环境正常工作。然而,我至今为止也没有看到一个100%安全的多线程对象池解决方案。
- 在本篇教程中,我只关心对象池处理的一种稀缺资源类型:内存。但是,其它类型的对象池也很重要。只是超出了本教程的范围。
同样,我们假定没有其它进程正在等待你尽快释放内存。这意味着重置是可以延迟的,而且对象池没有动态减少占用内存的功能。
带有初始化和重置功能的基本对象池
我们修正的ObjectPool <T>类如下所示:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public
class
ObjectPool<T> where T :
class
,
new
() {
private
Stack<T> m_objectStack;
private
Action<T> m_resetAction;
private
Action<T> m_onetimeInitAction;
public
ObjectPool(
int
initialBufferSize, Action<T>
ResetAction =
null
, Action<T> OnetimeInitAction =
null
)
{
m_objectStack =
new
Stack<T>(initialBufferSize);
m_resetAction = ResetAction;
m_onetimeInitAction = OnetimeInitAction;
}
public
T New()
{
if
(m_objectStack.Count > 0)
{
T t = m_objectStack.Pop();
if
(m_resetAction !=
null
)
m_resetAction(t);
return
t;
}
else
{
T t =
new
T();
if
(m_onetimeInitAction !=
null
)
m_onetimeInitAction(t);
return
t;
}
}
public
void
Store(T obj) { m_objectStack.Push(obj); } }
这种实现非常简单明了,参数T通过“whereT:class, new()”指定了两种限制方式。第一,T必须是一个类(毕竟,只有引用类型需要对象池),第二,它必须有无参构造函数。
构造函数使用估测的最大值作为对象池的第一个参数。其他两个参数都是可选参数。如果有值,则第一个用来重置对象池,第二个用来初始化一个新的对象池。ObjectPool<T>除构造函数外只有两个方法:New()、Store()。因为,对象池使用的是延迟重置方法,所以,所有工作都在New()方法中,即重新创建或者循环使用对象被初始化或重置之后。这就是两个可选参数的设计目的。下面是继承自MonoBehavior的对象池类:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class
SomeClass : MonoBehaviour
{
private
ObjectPool<List<Vector3>> m_poolOfListOfVector3 =
new
ObjectPool<List<Vector3>>(32, (list) =>{
list.Clear();
},
(list) => {
list.Capacity = 1024;
});
void
Update()
{
List<Vector3> listVector3 = m_poolOfListOfVector3.New();
// do stuff
m_poolOfListOfVector3.Store(listVector3);
}
}
如果你看过本系列教程的第一篇,就会知道从内存角度来说,在poolOfListOfVector3定义两个匿名委托函数是可以的。一方面,它们并非真的闭包而是“局部定义函数”,另一方面,这不重要因为对象池有类级别的作用范围。
可以让托管类型重置自身的对象池
对象池的基础版有了它应该有的功能,但它有一个概念性的缺陷。它违反了封装原则,没有把初始化/重置对象和对象类型的定义分开,导致了代码的紧耦合,这是应该要避免的。在上面的SomeClass 例子中,没有备选方案,因为我们不能改变List<T>的定义。然而,当你在对象池中使用了自定义类型对象,你可能希望它们执行IResetable 接口。相应地ObjectPoolWithReset<T>类也因此可以无需使用两个闭包作为参数(为了灵活性而保留的)。
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public
interface
IResetable
{
void
Reset(); }
public
class
ObjectPoolWithReset<T> where T :
class
, IResetable,
new
()
{
private
Stack<T> m_objectStack;
private
Action<T> m_resetAction;
private
Action<T> m_onetimeInitAction;
public
ObjectPoolWithReset(
int
initialBufferSize, Action<T>
ResetAction =
null
, Action<T> OnetimeInitAction =
null
)
{
m_objectStack =
new
Stack<T>(initialBufferSize);
m_resetAction = ResetAction;
m_onetimeInitAction = OnetimeInitAction;
}
public
T New()
{
if
(m_objectStack.Count > 0)
{
T t = m_objectStack.Pop();
t.Reset();
if
(m_resetAction !=
null
)
m_resetAction(t);
return
t;
}
else
{
T t =
new
T();
if
(m_onetimeInitAction !=
null
)
m_onetimeInitAction(t);
return
t;
}
}
public
void
Store(T obj) { m_objectStack.Push(obj); } }
带有整体重置功能的对象池
游戏中有些数据结构可能绝不会持续超过一个序列帧,而是在每帧结束前被释放。在这种情况下,如果很好的定义了所有对象重新存入对象池的时间点,那么这个对象池将更易用,效率也会更高。让我们先看看代码。
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public
class
ObjectPoolWithCollectiveReset<T> where T :
class
,
new
()
{
private
List<T> m_objectList;
private
int
m_nextAvailableIndex = 0;
private
Action<T> m_resetAction;
private
Action<T> m_onetimeInitAction;
public
ObjectPoolWithCollectiveReset(
int
initialBufferSize, Action<T>
ResetAction =
null
, Action<T> OnetimeInitAction =
null
)
{
m_objectList =
new
List<T>(initialBufferSize);
m_resetAction = ResetAction;
m_onetimeInitAction = OnetimeInitAction;
}
public
T New()
{
if
(m_nextAvailableIndex < m_objectList.Count)
{
// an allocated object is already available; just reset it
T t = m_objectList[m_nextAvailableIndex];
m_nextAvailableIndex++;
if
(m_resetAction !=
null
)
m_resetAction(t);
return
t;
}
else
{
// no allocated object is available
T t =
new
T();
m_objectList.Add(t);
m_nextAvailableIndex++;
if
(m_onetimeInitAction !=
null
)
m_onetimeInitAction(t);
return
t;
}
}
public
void
ResetAll() { m_nextAvailableIndex = 0; } }
改写之后的版本与最初版本基本一致。只是Store()被ResetAll()取代,这样当所有创建的对象被存入对象池时只需要调用一次。在类的内部,存储所有(甚至是正在被使用的)对象引用的Stack<T>被替换为List<T>,我们在list中也跟踪记录了最近被创建或释放对象的索引。那样的话,New()可以知道是要创建一个新的对象还是重置一个已存在对象。
0 0
- Unity中的C#内存管理(一)
- Unity中的C#内存管理(二)
- Unity中的C#内存管理(三)
- 【Unity教程】Unity中的C#内存管理
- 【Unity教程】Unity中的C#内存管理
- c#中的内存管理(一)开篇
- 【Unity】Unity 3D中的内存管理
- C#内存管理一
- Unity开发者的C#内存管理(上篇)
- Unity中的C#学习(一)
- Unity开发者的C#内存管理
- 浅谈C++中的内存管理(一)
- C++中的内存管理(一)
- IOS开发中的内存管理(一)
- Unity 3D中的内存管理
- Unity 3D中的内存管理
- Unity 3D中的内存管理
- Unity 3D中的内存管理
- python 处理抓取网页乱码问题
- 表单序列化
- 面向对象的六大原则
- iOS 如何布局
- Leetcode 242. Valid Anagram
- Unity中的C#内存管理(一)
- linux格式化磁盘命令
- 将一个整数转换为单个字符输出函数实现的细节性问题
- Android之 MTP框架和流程分析 (1)
- [Elasticsearch] 聚合中的重要概念 - Buckets(桶)及Metrics(指标)
- Unity中的C#内存管理(二)
- Merge Sorted Array
- iOS学习之——实例变量
- 软件开发过程-代码性能分析