|
1 | | -## 对象池 Object Pool |
| 1 | +## Unity3D内存管理——对象池(Object Pool) |
| 2 | + |
| 3 | + |
| 4 | +原文地址:[C# Memory Management for Unity Developers (part 3 of 3)](http://www.gamasutra.com/blogs/WendelinReich/20131127/203843/C_Memory_Management_for_Unity_Developers_part_3_of_3.php) |
| 5 | +其实从原文标题可以看出,这是一系列文章中的第三篇,前两篇讲解了从C#语言本身优化内存和Unity3D Profiler的使用,都很精彩,有兴趣的童鞋可以参考一下。 |
| 6 | +[C# Memory Management for Unity Developers (part 1 of 3)](http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php) |
| 7 | +[C# Memory Management for Unity Developers (part 2 of 3)](http://www.gamasutra.com/blogs/WendelinReich/20131119/203842/C_Memory_Management_for_Unity_Developers_part_2_of_3.php) |
| 8 | + |
| 9 | +### 从一个简单的对象池类开始说起 |
| 10 | +对象池背后的理念其实是非常简单的。我们将对象存储在一个池子中,当需要时在再次使用,而不是每次都实例化一个新的对象。池的最重要的特性,也就是对象池设计模式的本质是允许我们获取一个“新的”对象而不管它真的是一个新的对象还是循环使用的对象。该模式可以用以下简单的几行代码实现: |
| 11 | +```C# |
| 12 | +public class ObjectPool<T> where T : class, new() |
| 13 | +{ |
| 14 | + private Stack<T> m_objectStack = new Stack<T>(); |
| 15 | + |
| 16 | + public T New() |
| 17 | + { |
| 18 | + return (m_objectStack.Count == 0) ? new T() : m_objectStack.Pop(); |
| 19 | + } |
| 20 | + |
| 21 | + public void Store(T t) |
| 22 | + { |
| 23 | + m_objectStack.Push(t); |
| 24 | + } |
| 25 | +} |
| 26 | +``` |
| 27 | +很简单,也很好地体现了该模式的核心。如果你不太理解”where T”,没关系,稍后会解释的。如何使用呢?很简单,你只需要找到之前使用new操作符的表达式,例如: |
| 28 | +```C# |
| 29 | +void Update() |
| 30 | +{ |
| 31 | + MyClass m = new MyClass(); |
| 32 | +} |
| 33 | +``` |
| 34 | +然后将其替换为New()和Store()。 |
| 35 | +```C# |
| 36 | +ObjectPool<MyClass> poolOfMyClass = new ObjectPool<MyClass>(); |
| 37 | + |
| 38 | +void Update() |
| 39 | +{ |
| 40 | + MyClass m = poolOfMyClass.New(); |
| 41 | + |
| 42 | + // do stuff... |
| 43 | +
|
| 44 | + poolOfMyClass.Store(m); |
| 45 | +} |
| 46 | +``` |
| 47 | +### 增加点复杂度 |
| 48 | +我是简洁主义的忠实信徒,但就目前而言ObjectPool类或许过于简单了。如果你搜索下用C#实现的对象池类库,你会发现其中很多是相当复杂的。我们先暂停一下,仔细想想在一个通用的对象池中到底哪些是我们需要的,哪些是不需要的: |
| 49 | + |
| 50 | +* 很多类型的对象被重新使用前,在某些情况下,需要被reset。至少,所有的成员变量都要设置成初始值。这可以在池中实现而不需要用户处理。何时和如何重置需要考虑以下两个方面: |
| 51 | +* 重置是立即的(例如,在存储对象时即重置)还是延迟的(例如,在对象被重新使用后重置)。 |
| 52 | +* 重置是被池管理(例如,对于被放入池中的对象来说是透明的)还是声明池对象的类。 |
| 53 | +* 在上面的例子中,poolofMyClass池对象需要显示申明在类级别作用域。显然,当我们需要一个其它类型的对象池时就需要重新申明一个。或许我们可以实现一个对用户透明。 |
| 54 | +* 创建管理所有类型池的ObjectPool。 |
| 55 | +* 一些对象池类库管理了太多种类的可怕的资源(如内存,数据库连接,游戏对象,外部资产等)。这无疑增加了对象池的代码复杂度。 |
| 56 | +* 某些类型的资源是很珍贵的(如数据库连接),池需要显示上限并提供一个针对分配对象失败的安全措施; |
| 57 | +* 当池中对象很多却很少使用时,或许需要收缩的功能(不管是自动的还是强制的)。 |
| 58 | +* 最后,池可以被多个线程共享,因此需要实现为线程安全的。 |
| 59 | + |
| 60 | + |
| 61 | +那么其中那些是必需的呢?你的答案或许和我的不一样,但请允许我阐述我的观点: |
| 62 | + |
| 63 | +* 重置是必需的。但是正如你将在下面看的那样,我并没有强制到底是在池中还是被管理类中处理重置逻辑。你可能两种都需要,之后的代码中我将向你展示各自两个版本。 |
| 64 | +* Unity强制限制多线程。你可以在主线程中定义工作者线程,但只有主线程可以调用Unity API。以我的经验看来,我们并不需要将池实现为支持多线程。 |
| 65 | +* 仅个人而言,我并不介意每次为一个类型申明一个新的池。可选的方案是采用单例模式:创建一个新的对象池并放置于存储池的字典中,该字典放置在一个静态变量中。为了安全使用,你需要将将你的对象池实现为支持多线程。但就我看到的对象池而言没有一个是100%安全的。 |
| 66 | +* 在本篇文章中我重点处理内存。其它类型资源池也是很重要的,但超出本篇文章的范围。这很大程度上减少了以下的需求: |
| 67 | + * 不需要一个作限制用的最大值。如果你的游戏使用太多的资源,你已经陷入麻烦了,对象池也救不了你。 |
| 68 | + * 我们也可以假设没有其它进程等待你尽快释放内存。这就意味着重置可以是延迟的,也不需要提供收缩功能。 |
| 69 | + |
| 70 | +### 基本的实现初始化和重置的池(A basic pool with initialization and reset) |
| 71 | +修订后的版本如下: |
| 72 | +```C# |
| 73 | +public class ObjectPool<T> where T : class, new() |
| 74 | +{ |
| 75 | + private Stack<T> m_objectStack; |
| 76 | + |
| 77 | + private Action<T> m_resetAction; |
| 78 | + private Action<T> m_onetimeInitAction; |
| 79 | + |
| 80 | + public ObjectPool(int initialBufferSize, Action<T> |
| 81 | + ResetAction = null, Action<T> OnetimeInitAction = null) |
| 82 | + { |
| 83 | + m_objectStack = new Stack<T>(initialBufferSize); |
| 84 | + m_resetAction = ResetAction; |
| 85 | + m_onetimeInitAction = OnetimeInitAction; |
| 86 | + } |
| 87 | + |
| 88 | + public T New() |
| 89 | + { |
| 90 | + if (m_objectStack.Count > 0) |
| 91 | + { |
| 92 | + T t = m_objectStack.Pop(); |
| 93 | + |
| 94 | + if (m_resetAction != null) |
| 95 | + m_resetAction(t); |
| 96 | + |
| 97 | + return t; |
| 98 | + } |
| 99 | + else |
| 100 | + { |
| 101 | + T t = new T(); |
| 102 | + |
| 103 | + if (m_onetimeInitAction != null) |
| 104 | + m_onetimeInitAction(t); |
| 105 | + |
| 106 | + return t; |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + public void Store(T obj) |
| 111 | + { |
| 112 | + m_objectStack.Push(obj); |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | +该实现非常简单直白。参数T被指明为”where T:class,new()”,意味着有两个限制。首先,T必须为一个类(毕竟,只有引用类型需要被obejct-pool);其次,它必须要有一个无参构造函数。 |
| 117 | + |
| 118 | +构造函数将池可能的最大值作为第一个参数。另外两个是可选的闭包,如果传入值,第一个闭包将用来重置池,第二个初始化一个新的对象。除了构造函数外,ObjectPool<T>只有两个方法:New()和Store()。因为池使用了延迟策略,主要的工作在于New()。其中,新的和循环使用的对象要么被实例化,要么被重置,这两个操作通过传入的闭包实现。以下是池的使用方法: |
| 119 | +```C# |
| 120 | +class SomeClass : MonoBehaviour |
| 121 | +{ |
| 122 | + private ObjectPool<List<Vector3>> m_poolOfListOfVector3 = |
| 123 | + //32为假设的最大数量 |
| 124 | + new ObjectPool<List<Vector3>>(32, |
| 125 | + (list) => { |
| 126 | + list.Clear(); |
| 127 | + }, |
| 128 | + (list) => { |
| 129 | + //初始化容量为1024 |
| 130 | + list.Capacity = 1024; |
| 131 | + }); |
| 132 | + |
| 133 | + void Update() |
| 134 | + { |
| 135 | + List<Vector3> listVector3 = m_poolOfListOfVector3.New(); |
| 136 | + |
| 137 | + // do stuff |
| 138 | +
|
| 139 | + m_poolOfListOfVector3.Store(listVector3); |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | +### 被管理类型自重置的池(A pool that lets the managed type reset itself) |
| 144 | + |
| 145 | +上述的对象池实现了基本功能,但还是有瑕疵。它将初始化和重置对象在对象定义中分开了,在一定程度了违反了封装原则。导致了紧耦合,这是需要尽可能避免的。在上述SomeClass中,我们是没有真正的替代方案的,因为我们不能修改List<T>的定义。然而,当你用自定义类时,你可以实现IResetable接口作为代替。对应的ObjectPoolWithReset<T>也可以不需要指明两个闭包了(请注意,为了灵活性我还是留下了)。 |
| 146 | +```C# |
| 147 | +public interface IResetable |
| 148 | +{ |
| 149 | + void Reset(); |
| 150 | +} |
| 151 | + |
| 152 | +public class ObjectPoolWithReset<T> where T : class, IResetable, new() |
| 153 | +{ |
| 154 | + private Stack<T> m_objectStack; |
| 155 | + |
| 156 | + private Action<T> m_resetAction; |
| 157 | + private Action<T> m_onetimeInitAction; |
| 158 | + |
| 159 | + public ObjectPoolWithReset(int initialBufferSize, Action<T> |
| 160 | + ResetAction = null, Action<T> OnetimeInitAction = null) |
| 161 | + { |
| 162 | + m_objectStack = new Stack<T>(initialBufferSize); |
| 163 | + m_resetAction = ResetAction; |
| 164 | + m_onetimeInitAction = OnetimeInitAction; |
| 165 | + } |
| 166 | + |
| 167 | + public T New() |
| 168 | + { |
| 169 | + if (m_objectStack.Count > 0) |
| 170 | + { |
| 171 | + T t = m_objectStack.Pop(); |
| 172 | + //自行重置 |
| 173 | + t.Reset(); |
| 174 | + |
| 175 | + if (m_resetAction != null) |
| 176 | + m_resetAction(t); |
| 177 | + |
| 178 | + return t; |
| 179 | + } |
| 180 | + else |
| 181 | + { |
| 182 | + T t = new T(); |
| 183 | + |
| 184 | + if (m_onetimeInitAction != null) |
| 185 | + m_onetimeInitAction(t); |
| 186 | + |
| 187 | + return t; |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + public void Store(T obj) |
| 192 | + { |
| 193 | + m_objectStack.Push(obj); |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | +### 集体重置池(A pool with collective reset) |
| 198 | +有一些类型不需要在一系列帧中存留,仅在帧结束前就失效了。在这种情况下,我们可以在一个合适的时机将所有已经池化的对象(pooled objects)再次存储于池中。现在,我们重写该池使之更加简单高效。 |
| 199 | +```C# |
| 200 | +public class ObjectPoolWithCollectiveReset<T> where T : class, new() |
| 201 | +{ |
| 202 | + private List<T> m_objectList; |
| 203 | + private int m_nextAvailableIndex = 0; |
| 204 | + |
| 205 | + private Action<T> m_resetAction; |
| 206 | + private Action<T> m_onetimeInitAction; |
| 207 | + |
| 208 | + public ObjectPoolWithCollectiveReset(int initialBufferSize, Action<T> |
| 209 | + ResetAction = null, Action<T> OnetimeInitAction = null) |
| 210 | + { |
| 211 | + m_objectList = new List<T>(initialBufferSize); |
| 212 | + m_resetAction = ResetAction; |
| 213 | + m_onetimeInitAction = OnetimeInitAction; |
| 214 | + } |
| 215 | + |
| 216 | + public T New() |
| 217 | + { |
| 218 | + if (m_nextAvailableIndex < m_objectList.Count) |
| 219 | + { |
| 220 | + // an allocated object is already available; just reset it |
| 221 | + T t = m_objectList[m_nextAvailableIndex]; |
| 222 | + m_nextAvailableIndex++; |
| 223 | + |
| 224 | + if (m_resetAction != null) |
| 225 | + m_resetAction(t); |
| 226 | + |
| 227 | + return t; |
| 228 | + } |
| 229 | + else |
| 230 | + { |
| 231 | + // no allocated object is available |
| 232 | + T t = new T(); |
| 233 | + m_objectList.Add(t); |
| 234 | + m_nextAvailableIndex++; |
| 235 | + |
| 236 | + if (m_onetimeInitAction != null) |
| 237 | + m_onetimeInitAction(t); |
| 238 | + |
| 239 | + return t; |
| 240 | + } |
| 241 | + } |
| 242 | + |
| 243 | + public void ResetAll() |
| 244 | + { |
| 245 | + //重置索引 |
| 246 | + m_nextAvailableIndex = 0; |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | +相比于原始的ObjectPool <T> ,改动还是蛮大的。先不管类的签名,可以看到,Store()已经被ResetAll()代替了,且仅在所有已经分配的对象需要被放入池中时调用一次。在类内部,Stack<T>被List<T>代替,其中保存了所有已分配的对象(包括正在使用的对象)的引用。我们也可以跟踪最近创建或释放的对象在list中索引,由此,New()便可以知道是创建一个新的对象还是重置一个已有的对象。 |
| 251 | +### 拓展阅读(非译文部分) |
| 252 | +原文地址:http://www.gamasutra.com/blogs/WendelinReich/20131127/203843/C_Memory_Management_for_Unity_Developers_part_3_of_3.php |
2 | 253 |
|
0 commit comments