Unity 遊戲框架搭建 (二十) 更安全的對象池

Unity 遊戲框架搭建 (二十) 更安全的對象池

來自專欄涼鞋的筆記4 人贊了文章

上篇文章介紹了,只需通過實現 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 寫成 SimpleObjectPool,而如果恰好你的工程里有 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... x50msgPool.CurCount:50msgPool.CurCount:49OnRecycledmsgPool.CurCount:50msgPool.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... x50msgPool.CurCount:50msgPool.CurCount:49OnRecycledmsgPool.CurCount:50msgPool.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,今天就到這裡。

相關鏈接:

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

教程源碼:github.com/liangxiegame

QFramework&遊戲框架搭建QQ交流群: 623597263

轉載請註明地址:涼鞋的筆記liangxiegame.com/

微信公眾號:liangxiegame

weixin.qq.com/r/gjumvif (二維碼自動識別)

如果有幫助到您:

如果覺得本篇教程對您有幫助,不妨通過以下方式贊助筆者一下,鼓勵筆者繼續寫出更多高質量的教程,也讓更多的力量加入 QFramework 。

  • 給 QFramework 一個 Star
  • 地址: github.com/liangxiegame
  • 給 Asset Store 上的 QFramework 並給個五星(需要先下載)
  • 地址: u3d.as/SJ9
  • 購買 gitchat 話題《Unity 遊戲框架搭建:我所理解的框架》
  • 價格: 6 元,會員免費
  • 地址: gitbook.cn/gitchat/acti
  • 購買 gitchat 話題《Unity 遊戲框架搭建:資源管理神器 ResKit》
  • 價格: 6 元,會員免費
  • 地址: gitbook.cn/gitchat/acti
  • 購買同名電子書 :kancloud.cn/liangxiegam( 29.9 元,內容會在 2018 年 10 月份完結)

推薦閱讀:

(轉)遊戲開發 應用Docker實現開發環境
(轉)乾貨:Unity遊戲開發圖片紋理壓縮方案
Lua面向對象基礎
C#代碼跟unity關聯教程

TAG:unity | 遊戲開發 | 前端開發 |