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 ...
随机推荐
- [SDOI2011]工作安排
Description 你的公司接到了一批订单.订单要求你的公司提供n类产品,产品被编号为1~n,其中第i类产品共需要Ci件.公司共有m名员工,员工被编号为1~m员工能够制造的产品种类有所区别.一件产 ...
- 【JAVA零基础入门系列】Day5 Java中的运算符
运算符,顾名思义就是用于运算的符号,比如最简单的+-*/,这些运算符可以用来进行数学运算,举个最简单的栗子: 已知长方形的长为3cm,高为4cm,求长方形的面积. 好,我们先新建一个项目,命名为Rec ...
- PHP抓取网页图片
<?php set_time_limit(0);//抓取不受时间限制 if($_POST['Submit']=="开始抓取"){ $URL=$_POST['link']; g ...
- Python selenium 文件自动下载 (自动下载器)
MyGithub:https://github.com/williamzxl 最新代码已经上传到Github,以下版本为stupid版本. 由于在下载过程中需要下载不同文件,所以可以把所有类型放在Va ...
- 浅析php curl_multi_*系列函数进行批量http请求
何起: 一系列 数量很大 数据不热 还希望被蜘蛛大量抓取的页面,在蜘蛛抓取高峰时,响应时间会被拉得很高. 前人做了这样一个事儿:页面分3块,用3个内部接口提供,入口文件用curl_multi_*系列函 ...
- html阶段测试
1.简述src和href的区别? 2.在html页面的head中定义属性<meta http-equiv="X-UA-Compatible" content="IE ...
- SqlServer2008 导入导出txt或Execl数据
--右键user表所在的数据库,然后任务--导出数据,然后根据提示设置就行 --从txt中导入 EXEC master..xp_cmdshell 'bcp Northwind.dbo.sysusers ...
- 深刻理解反射(Reflection)
最近公司在搞自动化测试,由于版权问题,无法用 '录制脚本' 进行,也就没法用 VS 自带的 UITest 框架(蛋疼), 所以只能开源的 FlaUI 框架来搞了.其中不可避免的涉及到反射的应用,但自己 ...
- 轻松几句搞定【Javascript中的this指向】问题
this关键字在JavaScript中扮演了至关重要的角色,每次它的出现都伴随着它的指向问题,这也是很多初学者容易出错的地方. 不过,这篇文章将会带你一次性搞定this指向的问题,望能给大家提供帮助! ...
- selenium python grid
学习自动化一直都是在本机操作,感觉这样能够减少工作量确实很少.最近研究了一下分布式操作. 开始的想法是,我在一台机器上启动脚本,然后让脚本在不同机器的不同版本的浏览器上进行跑脚本. 需要准备的东西: ...