Unity 游戏框架搭建 (二十) 更安全的对象池
上篇文章介绍了,只需通过实现 IObjectFactory 接口和继承 Pool 类,就可以很方便地实现一个SimpleObjectPool。SimpleObjectPool 可以满足大部分的对象池的需求。而笔者通常将 SimpleObjectPool 用于项目开发,原因是接入比较方便,适合在发现性能瓶颈时迅速接入,不需要更改瓶颈对象的内部代码,而且代码精简较容易掌控。
本篇内容会较多:)
新的需求来了
当我们把对象池应用在框架开发中,我们就有了新的需求。
- 要保证使用时安全。
- 易用性。
现在让我们思考下 SimpleObjectPool 哪里不安全?
贴上 SimpleObjectPool 的源码:
public class SimpleObjectPool<T> : Pool<T>
{
readonly Action<T> mResetMethod;
public SimpleObjectPool(Func<T> factoryMethod, Action<T> resetMethod = null,int initCount = 0)
{
mFactory = new CustomObjectFactory<T>(factoryMethod);
mResetMethod = resetMethod;
for (int i = 0; i < initCount; i++)
{
mCacheStack.Push(mFactory.Create());
}
}
public override bool Recycle(T obj)
{
mResetMethod.InvokeGracefully(obj);
mCacheStack.Push(obj);
return true;
}
}
首先不安全的地方是泛型 T,在上篇文章中我们说泛型是灵活的体现,但是在框架设计中未约束的泛型却有可能是未知的隐患。我们很有可能在写代码时把 SimpleObjectPool<Fish> 写成 SimpleObjectPool<Fit>,而如果恰好你的工程里有 Fit 类,再加上使用var来声明变量而不是具体的类型(笔者较喜欢用var),那么这个错误要过好久才能发现。
为了解决这个问题,我们要给泛型T加上约束。要求可被对象池管理的对象必须是某种类型。是什么类型呢?就是IPoolAble类型。
public interface IPoolable
{
}
然后我们要给对象池类的泛型加上类型约束,本文的对象池我们叫SafeObjectPool。
public class SafeObjectPool<T> : Pool<T> where T : IPoolable
OK,第一个安全问题解决了。
第二个安全问题来了,我们有可能将一个 IPoolable 对象回收两次。为了解决这个问题,我们可以在SafeObjectPool 维护一个已经分配过的对象容器来记录对象是否被回收过,也可以在 IPoolable 对象中增加是否被回收的标记。这两种方式笔者倾向于后者,维护一个容器的成本相比只是在对象上增加标记的成本来说高太多了。
我们在 IPoolable 接口上增加一个 bool 变量来表示对象是否被回收过。
public interface IPoolAble
{
bool IsRecycled { get; set; }
}
接着在进行 Allocate 和 Recycle 时进行标记和拦截。
public class SafeObjectPool<T> : Pool<T> where T : IPoolAble
{
...
public override T Allocate()
{
T result = base.Allocate();
result.IsRecycled = false;
return result;
}
public override bool Recycle(T t)
{
if (t == null || t.IsRecycled)
{
return false;
}
t.IsRecycled = true;
mCacheStack.Push(t);
return true;
}
}
OK,第二个安全问题解决了。接下来第三个不是安全问题,是职责问题。我们再次观察下上篇文章中的 SimpleObjectPool
public class SimpleObjectPool<T> : Pool<T>
{
readonly Action<T> mResetMethod;
public SimpleObjectPool(Func<T> factoryMethod, Action<T> resetMethod = null,int initCount = 0)
{
mFactory = new CustomObjectFactory<T>(factoryMethod);
mResetMethod = resetMethod;
for (int i = 0; i < initCount; i++)
{
mCacheStack.Push(mFactory.Create());
}
}
public override bool Recycle(T obj)
{
mResetMethod.InvokeGracefully(obj);
mCacheStack.Push(obj);
return true;
}
}
可以看到,对象回收时的重置操作是由构造函数传进来的 mResetMethod 来完成的。当然,上篇忘记说了,这也是灵活的体现:)通过将重置的控制权开放给开发者,这样在接入 SimpleObjectPool 时,不需要更改对象内部的代码。
在框架设计中我们要收敛一些了,重置的操作要由对象自己来完成,我们要在 IPoolable 接口增加一个接收重置事件的方法。
public interface IPoolAble
{
void OnRecycled();
bool IsRecycled { get; set; }
}
当 SafeObjectPool 回收对象时来触发它。
public class SafeObjectPool<T> : Pool<T> where T : IPoolAble
{
...
public override bool Recycle(T t)
{
if (t == null || t.IsRecycled)
{
return false;
}
t.IsRecycled = true;
t.OnRecycled();
mCacheStack.Push(t);
return true;
}
}
同样地,在 SimpleObjectPool 中,创建对象的控制权我们也开放了出去,在 SafeObjectPool 中我们要收回来。还记得上篇文章的 CustomObjectFactory 嘛?
public class CustomObjectFactory<T> : IObjectFactory<T>
{
public CustomObjectFactory(Func<T> factoryMethod)
{
mFactoryMethod = factoryMethod;
}
protected Func<T> mFactoryMethod;
public T Create()
{
return mFactoryMethod();
}
}
CustomObjectFactory 不管要创建对象的构造方法是私有的还是公有的,只要开发者有办法搞出个对象就可以。现在我们要加上限制,大部分对象是 new 出来的。所以我们要设计一个可以 new 出对象的工厂。我们叫它 DefaultObjectFactory。
public class DefaultObjectFactory<T> : IObjectFactory<T> where T : new()
{
public T Create()
{
return new T();
}
}
注意下对泛型 T 的约束:) 接下来我们在构造 SafeObjectPool 时,创建一个 DefaultObjectFactory。
public class SafeObjectPool<T> : Pool<T> where T : IPoolAble, new()
{
public SafeObjectPool()
{
mFactory = new DefaultObjectFactory<T>();
}
...
注意 SafeObjectPool 的泛型也要加上 new() 的约束。 这样安全的 SafeObjectPool 已经完成了。 我们先测试下:
class Msg : IPoolAble
{
public void OnRecycled()
{
Log.I("OnRecycled");
}
public bool IsRecycled { get; set; }
}
private void Start()
{
var msgPool = new SafeObjectPool<Msg>();
msgPool.Init(100,50); // max count:100 init count: 50
Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
var fishOne = msgPool.Allocate();
Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
msgPool.Recycle(fishOne);
Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
for (int i = 0; i < 10; i++)
{
msgPool.Allocate();
}
Log.I("msgPool.CurCount:{0}", msgPool.CurCount);
}
由于是框架级的对象池,例子将上文的 Fish 改成 Msg。
输出结果:
OnRecycled
OnRecycled
... x50
msgPool.CurCount:50
msgPool.CurCount:49
OnRecycled
msgPool.CurCount:50
msgPool.CurCount:40
OK,测试结果没问题。不过,难道要让用户自己去维护 Msg 的对象池?
改进:
以上只是保证了机制的安全,这还不够。
我们想要用户获取一个 Msg 对象应该像 new Msg() 一样自然。要做到这样,我们需要做一些工作。
首先,Msg 的对象池全局只有一个就够了,为了实现这个需求,我们会想到用单例,但是 SafeObjectPool 已经继承了 Pool 了,不能再继承 QSingleton 了。还记得以前介绍的 QSingletonProperty 嘛?是时候该登场了,代码如下所示。
/// <summary>
/// Object pool.
/// </summary>
public class SafeObjectPool<T> : Pool<T>, ISingleton where T : IPoolAble, new()
{
#region Singleton
protected void OnSingletonInit()
{
}
public SafeObjectPool()
{
mFactory = new DefaultObjectFactory<T>();
}
public static SafeObjectPool<T> Instance
{
get { return QSingletonProperty<SafeObjectPool<T>>.Instance; }
}
public void Dispose()
{
QSingletonProperty<SafeObjectPool<T>>.Dispose();
}
#endregion
注意,构造方法的访问权限改成了 protected.
我们现在不想让用户通过 SafeObjectPool 来 Allocate 和 Recycle 池对象了,那么 Allocate 和 Recycle 的控制权就要交给池对象来管理。
由于控制权交给池对象管理这个需求不是必须的,所以我们要再提供一个接口
public interface IPoolType
{
void Recycle2Cache();
}
为什么只有一个 Recycle2Cache,没有 Allocate 相关的方法呢?
因为在池对象创建之前我们没有任何池对象,只能用静态方法创建。这就需要池对象提供一个静态的 Allocate 了。使用方法如下所示。
class Msg : IPoolAble,IPoolType
{
#region IPoolAble 实现
public void OnRecycled()
{
Log.I("OnRecycled");
}
public bool IsRecycled { get; set; }
#endregion
#region IPoolType 实现
public static Msg Allocate()
{
return SafeObjectPool<Msg>.Instance.Allocate();
}
public void Recycle2Cache()
{
SafeObjectPool<Msg>.Instance.Recycle(this);
}
#endregion
}
贴上测试代码:
SafeObjectPool<Msg>.Instance.Init(100, 50);
Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
var fishOne = Msg.Allocate();
Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
fishOne.Recycle2Cache();
Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
for (int i = 0; i < 10; i++)
{
Msg.Allocate();
}
Log.I("msgPool.CurCount:{0}", SafeObjectPool<Msg>.Instance.CurCount);
测试结果:
OnRecycled
OnRecycled
... x50
msgPool.CurCount:50
msgPool.CurCount:49
OnRecycled
msgPool.CurCount:50
msgPool.CurCount:40
测试结果一致,现在贴上 SafeObejctPool 的全部代码。这篇文章内容好多,写得我都快吐了- -。
using System;
/// <summary>
/// I cache type.
/// </summary>
public interface IPoolType
{
void Recycle2Cache();
}
/// <summary>
/// I pool able.
/// </summary>
public interface IPoolAble
{
void OnRecycled();
bool IsRecycled { get; set; }
}
/// <summary>
/// Count observer able.
/// </summary>
public interface ICountObserveAble
{
int CurCount { get; }
}
/// <summary>
/// Object pool.
/// </summary>
public class SafeObjectPool<T> : Pool<T>, ISingleton where T : IPoolAble, new()
{
#region Singleton
public void OnSingletonInit()
{
}
protected SafeObjectPool()
{
mFactory = new DefaultObjectFactory<T>();
}
public static SafeObjectPool<T> Instance
{
get { return QSingletonProperty<SafeObjectPool<T>>.Instance; }
}
public void Dispose()
{
QSingletonProperty<SafeObjectPool<T>>.Dispose();
}
#endregion
/// <summary>
/// Init the specified maxCount and initCount.
/// </summary>
/// <param name="maxCount">Max Cache count.</param>
/// <param name="initCount">Init Cache count.</param>
public void Init(int maxCount, int initCount)
{
if (maxCount > 0)
{
initCount = Math.Min(maxCount, initCount);
mMaxCount = maxCount;
}
if (CurCount < initCount)
{
for (int i = CurCount; i < initCount; ++i)
{
Recycle(mFactory.Create());
}
}
}
/// <summary>
/// Gets or sets the max cache count.
/// </summary>
/// <value>The max cache count.</value>
public int MaxCacheCount
{
get { return mMaxCount; }
set
{
mMaxCount = value;
if (mCacheStack != null)
{
if (mMaxCount > 0)
{
if (mMaxCount < mCacheStack.Count)
{
int removeCount = mMaxCount - mCacheStack.Count;
while (removeCount > 0)
{
mCacheStack.Pop();
--removeCount;
}
}
}
}
}
}
/// <summary>
/// Allocate T instance.
/// </summary>
public override T Allocate()
{
T result = base.Allocate();
result.IsRecycled = false;
return result;
}
/// <summary>
/// Recycle the T instance
/// </summary>
/// <param name="t">T.</param>
public override bool Recycle(T t)
{
if (t == null || t.IsRecycled)
{
return false;
}
if (mMaxCount > 0)
{
if (mCacheStack.Count >= mMaxCount)
{
t.OnRecycled();
return false;
}
}
t.IsRecycled = true;
t.OnRecycled();
mCacheStack.Push(t);
return true;
}
}
代码实现很简单,但是要考虑很多。
总结:
- SimpleObjectPool 适合用于项目开发,渐进式,更灵活。
- SafeObjectPool 适合用于库级开发,更多限制,要求开发者一开始就想好,更安全。
OK,今天就到这里。
转载请注明地址:凉鞋的笔记:liangxiegame.com
更多内容
QFramework 地址:https://github.com/liangxiegame/QFramework
QQ 交流群:623597263
Unity 进阶小班:
- 主要训练内容:
- 框架搭建训练(第一年)
- 跟着案例学 Shader(第一年)
- 副业的孵化(第二年、第三年)
- 权益、授课形式等具体详情请查看《小班产品手册》:https://liangxiegame.com/master/intro
- 主要训练内容:
关注公众号:liangxiegame 获取第一时间更新通知及更多的免费内容。
Unity 游戏框架搭建 (二十) 更安全的对象池的更多相关文章
- Unity 游戏框架搭建 (二十二) 简易引用计数器
引用计数是一个很好用的技术概念,不要被这个名字吓到了.首先来讲讲引用计数是干嘛的. 引用计数使用场景 有一间黑色的屋子,里边有一盏灯.当第一个人进屋的时候灯会打开,之后的人进来则不用再次打开了,因为已 ...
- Unity 游戏框架搭建 2019 (十八~二十) 概率函数 & GameObject 显示、隐藏简化 & 第二章 小结与快速复习
在笔者刚做项目的时候,遇到了一个需求.第一个项目是一个跑酷游戏,而跑酷游戏是需要一条一条跑道拼接成的.每个跑道的长度是固定的,而怪物的出现位置也是在跑道上固定好的.那么怪物出现的概率决定一部分关卡的难 ...
- Unity 游戏框架搭建 (二) 单例的模板
上一篇文章中说到的manager of managers,其中每个manager都是单例的实现,当然也可以使用静态类实现,但是相比于静态类的实现,单例的实现更为通用,可以适用大多数情况. 如何设计 ...
- Unity 游戏框架搭建 (二十三) 重构小工具 Platform
在日常开发中,我们经常遇到或者写出这样的代码 var sTrAngeNamingVariable = "a variable"; #if UNITY_IOS || UNITY_AN ...
- # Unity 游戏框架搭建 2019 (十六、十七) localPosition 简化与Transform 重置
在上一篇我们收集了一个 屏幕分辨率检测的一个小工具.今天呢再往下接着探索. 问题 我们今天在接着探索.不管是写 UI 还是写 GamePlay,多多少少都需要操作 Transform. 而在笔者刚接触 ...
- Unity 游戏框架搭建 (二十一) 使用对象池时的一些细节
上篇文章使用SafeObjectPool实现了一个简单的Msg类.代码如下: class Msg : IPoolAble,IPoolType { #region IPoolAble 实现 public ...
- Unity 游戏框架搭建 (十) QFramework v0.0.2小结
从框架搭建系列的第一篇文章开始到现在有四个多月时间了,这段时间对自己来说有很多的收获,好多小伙伴和前辈不管是在评论区还是私下里给出的建议非常有参考性,在此先谢过各位. 说到是一篇小节,先列出框架的概要 ...
- Unity 游戏框架搭建 (十三) 无需继承的单例的模板
之前的文章中介绍的Unity 游戏框架搭建 (二) 单例的模板和Unity 游戏框架搭建 (三) MonoBehaviour单例的模板有一些问题. 存在的问题: 只要继承了单例的模板就无法再继承其他的 ...
- Unity 游戏框架搭建 (十六) v0.0.1 架构调整
背景: 前段时间用Xamarin.OSX开发一些工具,遇到了两个问题. QFramework的大部分的类耦合了Unity的API,这样导致不能在其他CLR平台使用QFramework. QFramew ...
随机推荐
- bzoj4557【JLOI2016】侦查守卫
这道题对于我来说并不是特别简单,还可以. 更新一下blog 树形DP f[i][j]表示i的子树中,最高覆盖到i向下第j层的最小花费. g[i][j]表示i的子树全部覆盖,还能向上覆盖j层的最小花费. ...
- 课程作业02(关于Java的几点讨论)
---恢复内容开始--- 1.一个Java类文件中真的只能有一个公有类吗? public class Test { public static void main(String[] args) { } ...
- CPU工作方式、多核心、超线程技术详解[转贴]
CPU是一台电脑的灵魂,决定电脑整体性能.现在的主流CPU都是多核的,有的运用了多线程技术(Hyper-threading,简称HT).多核可能还容易理解些,相信不少玩家都能说出个所以然.但超线程是个 ...
- Hexo + GitHub Pages搭建博客
搭建 Node.js 环境 为什么要搭建 Node.js 环境? – 因为 Hexo 博客系统是基于 Node.js 编写的 Node.js 是一个基于 Chrome V8 引擎的 JavaScrip ...
- 关于http与https区别
http与https: http叫超文本传输协议,信息为明文传输.https是具有安全性的传输协议,是由http+ssl层,需要到ca申请证书,一般需要费用.信息为加密传输,需要验证用户身份.二者的端 ...
- c#使用GDI+简单绘图
private void button2_Click(object sender, EventArgs e) { Bitmap image = new Bitmap(200, 200); Graphi ...
- java类加载小记
java类只有当创建实体或被调用时才会加载,加载时按 编码顺序 先加载static后加载普通的.static模块和static变量都是同一等级的,谁写前面就先加载谁. 在调用某个静态类的方法时,会按编 ...
- 前端页面卡顿、也许是DOM操作惹的祸?
界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现 的.尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,所以前端性能优化 ...
- layer.msg 添加在Ajax之前 显示进度条。
一.使用方法:1)必须先引入jQuery1.8或以上版本 <script src="jQuery的路径"></script> <script src= ...
- WPF 绑定密码
我们发现我们无法绑定密码框的密码,PasswordBox 的 Password 不能绑定. 我们想做 MVVM ,我们需要绑定密码,不能使用前台 xaml.cs 监听 密码改变得到密码的值,传到 Vi ...