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 ...
随机推荐
- 手动添加 Git bash 到鼠标右键
由于不知原因,右键没有了Git Bash Here,没有这个右键菜单导致获取Git仓库中的代码很不方便,所以决定通过注册表的方式将这个菜单加出来. 1.win + R,输入"regedit& ...
- java启动监听错误: org.springframework.web.context.ContextLoaderListener
项目启动会报以下错误: 解决方案如下: 感谢好心人的提示“其实可能是你的jar文件没有同步发布到自己项目的lib目录中(如果你是用Maven进行构建的话) 可以试试 下面的办法 项目点击右键 点击 P ...
- EXISTS/NOT EXISTS CASE WHEN等使用方法
--简单判断用法 WHERE EXISTS (SELECT * FROM cpay..System_Setting) --可以替换count ) FROM cpay..System_Setting U ...
- Django学习中遇到的问题(1)django migration No migrations to apply
C:\Users\Desktop\homeWork\Django_stu_man>python manage.py makemigrations Migrations for 'app01': ...
- C# 使用Parallel并行开发Parallel.For、Parallel.Foreach实例
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.N ...
- java spark-streaming接收TCP/Kafka数据
本文将展示 1.如何使用spark-streaming接入TCP数据并进行过滤: 2.如何使用spark-streaming接入TCP数据并进行wordcount: 内容如下: 1.使用maven,先 ...
- 使用svn与maven管理的项目导入Eclipse,但是与本地svn客户端关联不上?
因为这个问题,导致我的项目导了删,删了导.现在终于弄明白了. 首先,需求场景是: 1.使用svn进行版本控制; 2.使用maven进行项目管理. 3.使用Tortoise svn将项 ...
- VS2015智能提示由英文改为中文
使用 VS2015 时,在 4.0 下智能提示显示中文,在 4.5 下显示英文,对于我这种爱(ying)国(yu)人(tai)士(lan)来说,用起来太不方便了,于 是在 知乎 上找到个好方法如下: ...
- 运维&网络知识(一)
1. DNS 域名系统(Domain Name System),因特网上作为域名和IP地址映射的一个分布式数据库.
- 企业微信开发之向员工付款(C#)
一.企业微信API 地址:http://work.weixin.qq.com/api/doc#11545 二.参数说明 1.向员工付款 请求方式:POST(HTTPS)请求地址:https://api ...