上篇文章介绍了,只需通过实现 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

更多内容

Unity 游戏框架搭建 (二十) 更安全的对象池的更多相关文章

  1. Unity 游戏框架搭建 (二十二) 简易引用计数器

    引用计数是一个很好用的技术概念,不要被这个名字吓到了.首先来讲讲引用计数是干嘛的. 引用计数使用场景 有一间黑色的屋子,里边有一盏灯.当第一个人进屋的时候灯会打开,之后的人进来则不用再次打开了,因为已 ...

  2. Unity 游戏框架搭建 2019 (十八~二十) 概率函数 & GameObject 显示、隐藏简化 & 第二章 小结与快速复习

    在笔者刚做项目的时候,遇到了一个需求.第一个项目是一个跑酷游戏,而跑酷游戏是需要一条一条跑道拼接成的.每个跑道的长度是固定的,而怪物的出现位置也是在跑道上固定好的.那么怪物出现的概率决定一部分关卡的难 ...

  3. Unity 游戏框架搭建 (二) 单例的模板

      上一篇文章中说到的manager of managers,其中每个manager都是单例的实现,当然也可以使用静态类实现,但是相比于静态类的实现,单例的实现更为通用,可以适用大多数情况. 如何设计 ...

  4. Unity 游戏框架搭建 (二十三) 重构小工具 Platform

    在日常开发中,我们经常遇到或者写出这样的代码 var sTrAngeNamingVariable = "a variable"; #if UNITY_IOS || UNITY_AN ...

  5. # Unity 游戏框架搭建 2019 (十六、十七) localPosition 简化与Transform 重置

    在上一篇我们收集了一个 屏幕分辨率检测的一个小工具.今天呢再往下接着探索. 问题 我们今天在接着探索.不管是写 UI 还是写 GamePlay,多多少少都需要操作 Transform. 而在笔者刚接触 ...

  6. Unity 游戏框架搭建 (二十一) 使用对象池时的一些细节

    上篇文章使用SafeObjectPool实现了一个简单的Msg类.代码如下: class Msg : IPoolAble,IPoolType { #region IPoolAble 实现 public ...

  7. Unity 游戏框架搭建 (十) QFramework v0.0.2小结

    从框架搭建系列的第一篇文章开始到现在有四个多月时间了,这段时间对自己来说有很多的收获,好多小伙伴和前辈不管是在评论区还是私下里给出的建议非常有参考性,在此先谢过各位. 说到是一篇小节,先列出框架的概要 ...

  8. Unity 游戏框架搭建 (十三) 无需继承的单例的模板

    之前的文章中介绍的Unity 游戏框架搭建 (二) 单例的模板和Unity 游戏框架搭建 (三) MonoBehaviour单例的模板有一些问题. 存在的问题: 只要继承了单例的模板就无法再继承其他的 ...

  9. Unity 游戏框架搭建 (十六) v0.0.1 架构调整

    背景: 前段时间用Xamarin.OSX开发一些工具,遇到了两个问题. QFramework的大部分的类耦合了Unity的API,这样导致不能在其他CLR平台使用QFramework. QFramew ...

随机推荐

  1. Sets 比赛时想错方向了。。。。 (大数不能处理负数啊)

    Sets Time Limit: 6000/3000MS (Java/Others) Memory Limit: 128000/64000KB (Java/Others) SubmitStatus P ...

  2. ServiceStack.Text / Newtonsoft.Json 两种json序列化性能比较

    JSON序列化现在应用非常多,尤其在前后端分离的情况下,平常大多数C#下都使用Newtonsoft.Json来操作,量少的情况下,还可以忽略,但量大的情况下就要考虑使用ServiceStack.Tex ...

  3. ubuntu远程桌面介绍

    一.windows远程ubuntu14.04 由于xrdp.gnome和unity之间的兼容性问题,在Ubuntu 14.04版本中仍然无法使用xrdp登陆gnome或unity的远程桌面,现象是登录 ...

  4. JavaScript 版数据结构与算法(四)集合

    今天,我们要讲的是数据结构与算法中的集合. 集合简介 什么是集合?与栈.队列.链表这些顺序数据结构不同,集合是一种无序且唯一的数据结构.集合有什么用?在 Python 中,我经常使用集合来给数组去重: ...

  5. 基于RTKLIB构建高并发通信测试工具

    1. RTKLIB基础动态库生成 RTKLIB是全球导航卫星系统GNSS(global navigation satellite system)的标准&精密定位开源程序包,由日本东京海洋大学的 ...

  6. C#无限级分类递归显示示例

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="RoleDemo20150305 ...

  7. 邮件实现详解(四)------JavaMail 发送(带图片和附件)和接收邮件

    好了,进入这个系列教程最主要的步骤了,前面邮件的理论知识我们都了解了,那么这篇博客我们将用代码完成邮件的发送.这在实际项目中应用的非常广泛,比如注册需要发送邮件进行账号激活,再比如OA项目中利用邮件进 ...

  8. Python开篇

    一:Python的前世今生 python的创始人为吉多·范罗苏姆(Guido van Rossum).1989年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的脚本解释程序,作为 ...

  9. CoreData和SQLite多线程访问时的线程安全问题

    数据库读取操作一般都是多线程访问的.在对数据进行读取时,我们要保证其当前状态不能被修改,即读取时加锁,否则就会出现数据错误混乱.IOS中常用的两种数据持久化存储方式:CoreData和SQLite,两 ...

  10. git 分支改名

    给一个git分支改名的方法很简单 如果对于分支不是当前分支,可以使用下面代码: git branch -m 原名 新 如果是当前,那么可以使用加上新名字 git branch -m 原名 参见: ht ...