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

上篇文章介紹了,只需通過實現 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 不管要創建對象的構造方法是私有的還是公有的,只要開發者有辦法搞出個對象就可以?,F在我們要加上限制,大部分對象是 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

更多內容

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 我們知道,Java 創建一個實例的消耗是不小的,如果沒有使用棧上分配和 TLAB,那么就需要使用 CAS 在...
    莫那一魯道閱讀 4,411評論 3 11
  • 一篇影評里一句話,精彩到我花了半個小時搜集作者的其他文章。 “看的出你汪洋恣肆的才華,也看的出這種才華未經梳理” ...
    心甲閱讀 258評論 1 1
  • 人間冷暖 世態炎涼 矯情似乎是每天生活的必需品 像是每次菜中必須有油鹽醬醋一樣 時常在想 什么時候自己才可以改變呢...
    車車車66閱讀 272評論 1 2
  • 我怕當我讀懂生活生活卻改變了未來的我 成長此間的向往,相遇,相識,相知,相守相隨的幸福與歡笑/離別與惆悵/期待及失...
    安小邁閱讀 246評論 0 0
  • 放完假了, 想給大家煮點好吃的, 天氣冷, 想著打火鍋最合適了, 吃麻辣還是咖喱味的呢? 和你商量, 選了大家都可...
    達瓦青措閱讀 474評論 0 3