Unity 游戏框架搭建 (二十) 更安全的对象池

上篇文章介绍了,只需通过实现 IObjectFactory 接口和继承 Pool 类,就可以很方便地实现一个SimpleObjectPool。SimpleObjectPool 可以满足大部分的对象池的需求。而笔者通常将 SimpleObjectPool 用于项目开发,原因是接入比较方便,适合在发现性能瓶颈时迅速接入,不需要更改瓶颈对象的内部代码,而且代码精简较容易掌控。

本篇内容会较多:)

新的需求来了

当我们把对象池应用在框架开发中,我们就有了新的需求。

  • 要保证使用时安全。
  • 易用性。现在让我们思考下 SimpleObjectPool 哪里不安全?

贴上 SimpleObjectPool 的源码:

  1. public class SimpleObjectPool<T> : Pool<T>
  2. {
  3. readonly Action<T> mResetMethod;
  4.  
  5. public SimpleObjectPool(Func<T> factoryMethod, Action<T> resetMethod = null,int initCount = 0)
  6. {
  7. mFactory = new CustomObjectFactory<T>(factoryMethod);
  8. mResetMethod = resetMethod;
  9.  
  10. for (int i = 0; i < initCount; i++)
  11. {
  12. mCacheStack.Push(mFactory.Create());
  13. }
  14. }
  15.  
  16. public override bool Recycle(T obj)
  17. {
  18. mResetMethod.InvokeGracefully(obj);
  19. mCacheStack.Push(obj);
  20. return true;
  21. }
  22. }

首先不安全的地方是泛型 T,在上篇文章中我们说泛型是灵活的体现,但是在框架设计中未约束的泛型却有可能是未知的隐患。我们很有可能在写代码时把 SimpleObjectPool\ 写成 SimpleObjectPool\,而如果恰好你的工程里有 Fit 类,再加上使用var来声明变量而不是具体的类型(笔者较喜欢用var),那么这个错误要过好久才能发现。

为了解决这个问题,我们要给泛型T加上约束。要求可被对象池管理的对象必须是某种类型。是什么类型呢?就是IPoolAble类型。

  1. public interface IPoolable
  2. {
  3.  
  4. }

然后我们要给对象池类的泛型加上类型约束,本文的对象池我们叫SafeObjectPool。

  1. public class SafeObjectPool<T> : Pool<T> where T : IPoolable

OK,第一个安全问题解决了。

第二个安全问题来了,我们有可能将一个 IPoolable 对象回收两次。为了解决这个问题,我们可以在SafeObjectPool 维护一个已经分配过的对象容器来记录对象是否被回收过,也可以在 IPoolable 对象中增加是否被回收的标记。这两种方式笔者倾向于后者,维护一个容器的成本相比只是在对象上增加标记的成本来说高太多了。

我们在 IPoolable 接口上增加一个 bool 变量来表示对象是否被回收过。

  1. public interface IPoolAble
  2. {
  3. bool IsRecycled { get; set; }
  4. }

接着在进行 Allocate 和 Recycle 时进行标记和拦截。

  1. public class SafeObjectPool<T> : Pool<T> where T : IPoolAble
  2. {
  3. ...
  4. public override T Allocate()
  5. {
  6. T result = base.Allocate();
  7. result.IsRecycled = false;
  8. return result;
  9. }
  10.  
  11. public override bool Recycle(T t)
  12. {
  13. if (t == null || t.IsRecycled)
  14. {
  15. return false;
  16. }
  17.  
  18. t.IsRecycled = true;
  19. mCacheStack.Push(t);
  20.  
  21. return true;
  22. }
  23. }

OK,第二个安全问题解决了。接下来第三个不是安全问题,是职责问题。我们再次观察下上篇文章中的SimpleObjectPool

  1. public class SimpleObjectPool<T> : Pool<T>
  2. {
  3. readonly Action<T> mResetMethod;
  4.  
  5. public SimpleObjectPool(Func<T> factoryMethod, Action<T> resetMethod = null,int initCount = 0)
  6. {
  7. mFactory = new CustomObjectFactory<T>(factoryMethod);
  8. mResetMethod = resetMethod;
  9.  
  10. for (int i = 0; i < initCount; i++)
  11. {
  12. mCacheStack.Push(mFactory.Create());
  13. }
  14. }
  15.  
  16. public override bool Recycle(T obj)
  17. {
  18. mResetMethod.InvokeGracefully(obj);
  19. mCacheStack.Push(obj);
  20. return true;
  21. }
  22. }

可以看到,对象回收时的重置操作是由构造函数传进来的 mResetMethod 来完成的。当然,上篇忘记说了,这也是灵活的体现:)通过将重置的控制权开放给开发者,这样在接入 SimpleObjectPool 时,不需要更改对象内部的代码。

在框架设计中我们要收敛一些了,重置的操作要由对象自己来完成,我们要在 IPoolable 接口增加一个接收重置事件的方法。

  1. public interface IPoolAble
  2. {
  3. void OnRecycled();
  4.  
  5. bool IsRecycled { get; set; }
  6. }

当 SafeObjectPool 回收对象时来触发它。

  1. public class SafeObjectPool<T> : Pool<T> where T : IPoolAble
  2. {
  3. ...
  4. public override bool Recycle(T t)
  5. {
  6. if (t == null || t.IsRecycled)
  7. {
  8. return false;
  9. }
  10.  
  11. t.IsRecycled = true;
  12. t.OnRecycled();
  13. mCacheStack.Push(t);
  14.  
  15. return true;
  16. }
  17. }

同样地,在 SimpleObjectPool 中,创建对象的控制权我们也开放了出去,在 SafeObjectPool 中我们要收回来。还记得上篇文章的 CustomObjectFactory 嘛?

  1. public class CustomObjectFactory<T> : IObjectFactory<T>
  2. {
  3. public CustomObjectFactory(Func<T> factoryMethod)
  4. {
  5. mFactoryMethod = factoryMethod;
  6. }
  7.  
  8. protected Func<T> mFactoryMethod;
  9.  
  10. public T Create()
  11. {
  12. return mFactoryMethod();
  13. }
  14. }

CustomObjectFactory 不管要创建对象的构造方法是私有的还是公有的,只要开发者有办法搞出个对象就可以。现在我们要加上限制,大部分对象是 new 出来的。所以我们要设计一个可以 new 出对象的工厂。我们叫它 DefaultObjectFactory。

  1. public class DefaultObjectFactory<T> : IObjectFactory<T> where T : new()
  2. {
  3. public T Create()
  4. {
  5. return new T();
  6. }
  7. }

注意下对泛型 T 的约束:)

接下来我们在构造 SafeObjectPool 时,创建一个 DefaultObjectFactory。

  1. public class SafeObjectPool<T> : Pool<T> where T : IPoolAble, new()
  2. {
  3. public SafeObjectPool()
  4. {
  5. mFactory = new DefaultObjectFactory<T>();
  6. }
  7. ...

注意 SafeObjectPool 的泛型也要加上 new() 的约束。

这样安全的 SafeObjectPool 已经完成了。

我们先测试下:

  1. class Msg : IPoolAble
  2. {
  3. public void OnRecycled()
  4. {
  5. Log.I("OnRecycled");
  6. }
  7.  
  8. public bool IsRecycled { get; set; }
  9. }
  10.  
  11. private void Start()
  12. {
  13. var msgPool = new SafeObjectPool<Msg>();
  14.  
  15. msgPool.Init(100,50); // max count:100 init count: 50
  16.  
  17. Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
  18.  
  19. var fishOne = msgPool.Allocate();
  20.  
  21. Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
  22.  
  23. msgPool.Recycle(fishOne);
  24.  
  25. Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
  26.  
  27. for (int i = 0; i < 10; i++)
  28. {
  29. msgPool.Allocate();
  30. }
  31.  
  32. Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
  33. }

由于是框架级的对象池,例子将上文的 Fish 改成 Msg。

输出结果:

  1. OnRecycled
  2. OnRecycled
  3. ... x50
  4. msgPool.CurCount:50
  5. msgPool.CurCount:49
  6. OnRecycled
  7. msgPool.CurCount:50
  8. msgPool.CurCount:40

OK,测试结果没问题。不过,难道要让用户自己去维护 Msg 的对象池?

改进:

以上只是保证了机制的安全,这还不够。我们想要用户获取一个 Msg 对象应该像 new Msg() 一样自然。要做到这样,我们需要做一些工作。

首先,Msg 的对象池全局只有一个就够了,为了实现这个需求,我们会想到用单例,但是 SafeObjectPool 已经继承了 Pool 了,不能再继承 QSingleton 了。还记得以前介绍的 QSingletonProperty 嘛?是时候该登场了,代码如下所示。

  1. /// <summary>
  2. /// Object pool.
  3. /// </summary>
  4. public class SafeObjectPool<T> : Pool<T>, ISingleton where T : IPoolAble, new()
  5. {
  6. #region Singleton
  7. protected void OnSingletonInit()
  8. {
  9. }
  10.  
  11. public SafeObjectPool()
  12. {
  13. mFactory = new DefaultObjectFactory<T>();
  14. }
  15.  
  16. public static SafeObjectPool<T> Instance
  17. {
  18. get { return QSingletonProperty<SafeObjectPool<T>>.Instance; }
  19. }
  20.  
  21. public void Dispose()
  22. {
  23. QSingletonProperty<SafeObjectPool<T>>.Dispose();
  24. }
  25. #endregion

注意,构造方法的访问权限改成了 protected.

我们现在不想让用户通过 SafeObjectPool 来 Allocate 和 Recycle 池对象了,那么 Allocate 和 Recycle 的控制权就要交给池对象来管理。

由于控制权交给池对象管理这个需求不是必须的,所以我们要再提供一个接口

  1. public interface IPoolType
  2. {
  3. void Recycle2Cache();
  4. }

为什么只有一个 Recycle2Cache,没有 Allocate 相关的方法呢?因为在池对象创建之前我们没有任何池对象,只能用静态方法创建。这就需要池对象提供一个静态的 Allocate 了。使用方法如下所示。

  1. class Msg : IPoolAble,IPoolType
  2. {
  3. #region IPoolAble 实现
  4.  
  5. public void OnRecycled()
  6. {
  7. Log.I("OnRecycled");
  8. }
  9.  
  10. public bool IsRecycled { get; set; }
  11.  
  12. #endregion
  13.  
  14.  
  15. #region IPoolType 实现
  16.  
  17. public static Msg Allocate()
  18. {
  19. return SafeObjectPool<Msg>.Instance.Allocate();
  20. }
  21.  
  22. public void Recycle2Cache()
  23. {
  24. SafeObjectPool<Msg>.Instance.Recycle(this);
  25. }
  26.  
  27. #endregion
  28. }

贴上测试代码:

  1. SafeObjectPool<Msg>.Instance.Init(100, 50);
  2.  
  3. Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
  4.  
  5. var fishOne = Msg.Allocate();
  6.  
  7. Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
  8.  
  9. fishOne.Recycle2Cache();
  10.  
  11. Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
  12.  
  13. for (int i = 0; i < 10; i++)
  14. {
  15. Msg.Allocate();
  16. }
  17.  
  18. Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);

测试结果:

  1. OnRecycled
  2. OnRecycled
  3. ... x50
  4. msgPool.CurCount:50
  5. msgPool.CurCount:49
  6. OnRecycled
  7. msgPool.CurCount:50
  8. msgPool.CurCount:40

测试结果一致,现在贴上 SafeObejctPool 的全部代码。这篇文章内容好多,写得我都快吐了- -。

  1. using System;
  2.  
  3. /// <summary>
  4. /// I cache type.
  5. /// </summary>
  6. public interface IPoolType
  7. {
  8. void Recycle2Cache();
  9. }
  10.  
  11. /// <summary>
  12. /// I pool able.
  13. /// </summary>
  14. public interface IPoolAble
  15. {
  16. void OnRecycled();
  17.  
  18. bool IsRecycled { get; set; }
  19. }
  20.  
  21. /// <summary>
  22. /// Count observer able.
  23. /// </summary>
  24. public interface ICountObserveAble
  25. {
  26. int CurCount { get; }
  27. }
  28.  
  29. /// <summary>
  30. /// Object pool.
  31. /// </summary>
  32. public class SafeObjectPool<T> : Pool<T>, ISingleton where T : IPoolAble, new()
  33. {
  34. #region Singleton
  35. public void OnSingletonInit()
  36. {
  37. }
  38.  
  39. protected SafeObjectPool()
  40. {
  41. mFactory = new DefaultObjectFactory<T>();
  42. }
  43.  
  44. public static SafeObjectPool<T> Instance
  45. {
  46. get { return QSingletonProperty<SafeObjectPool<T>>.Instance; }
  47. }
  48.  
  49. public void Dispose()
  50. {
  51. QSingletonProperty<SafeObjectPool<T>>.Dispose();
  52. }
  53. #endregion
  54.  
  55.  
  56. /// <summary>
  57. /// Init the specified maxCount and initCount.
  58. /// </summary>
  59. /// <param name="maxCount">Max Cache count.</param>
  60. /// <param name="initCount">Init Cache count.</param>
  61. public void Init(int maxCount, int initCount)
  62. {
  63. if (maxCount > 0)
  64. {
  65. initCount = Math.Min(maxCount, initCount);
  66.  
  67. mMaxCount = maxCount;
  68. }
  69.  
  70. if (CurCount < initCount)
  71. {
  72. for (int i = CurCount; i < initCount; ++i)
  73. {
  74. Recycle(mFactory.Create());
  75. }
  76. }
  77. }
  78.  
  79. /// <summary>
  80. /// Gets or sets the max cache count.
  81. /// </summary>
  82. /// <value>The max cache count.</value>
  83. public int MaxCacheCount
  84. {
  85. get { return mMaxCount; }
  86. set
  87. {
  88. mMaxCount = value;
  89.  
  90. if (mCacheStack != null)
  91. {
  92. if (mMaxCount > 0)
  93. {
  94. if (mMaxCount < mCacheStack.Count)
  95. {
  96. int removeCount = mMaxCount - mCacheStack.Count;
  97. while (removeCount > 0)
  98. {
  99. mCacheStack.Pop();
  100. --removeCount;
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }
  107.  
  108. /// <summary>
  109. /// Allocate T instance.
  110. /// </summary>
  111. public override T Allocate()
  112. {
  113. T result = base.Allocate();
  114. result.IsRecycled = false;
  115. return result;
  116. }
  117.  
  118. /// <summary>
  119. /// Recycle the T instance
  120. /// </summary>
  121. /// <param name="t">T.</param>
  122. public override bool Recycle(T t)
  123. {
  124. if (t == null || t.IsRecycled)
  125. {
  126. return false;
  127. }
  128.  
  129. if (mMaxCount > 0)
  130. {
  131. if (mCacheStack.Count >= mMaxCount)
  132. {
  133. t.OnRecycled();
  134. return false;
  135. }
  136. }
  137.  
  138. t.IsRecycled = true;
  139. t.OnRecycled();
  140. mCacheStack.Push(t);
  141.  
  142. return true;
  143. }
  144. }

代码实现很简单,但是要考虑很多。

总结:

  • SimpleObjectPool 适合用于项目开发,渐进式,更灵活。
  • SafeObjectPool 适合用于库级开发,更多限制,要求开发者一开始就想好,更安全。OK,今天就到这里。

相关链接:

我的框架地址:https://github.com/liangxiegame/QFramework

教程源码:https://github.com/liangxiegame/QFramework/tree/master/Assets/HowToWriteUnityGameFramework/

QFramework &游戏框架搭建QQ交流群: 623597263

转载请注明地址:凉鞋的笔记http://liangxiegame.com/

微信公众号:liangxiegame

20.更安全的对象池  - 图1

如果有帮助到您:

如果觉得本篇教程对您有帮助,不妨通过以下方式赞助笔者一下,鼓励笔者继续写出更多高质量的教程,也让更多的力量加入 QFramework 。