Unity 游戏框架搭建 (二十) 更安全的对象池

发表于2017-11-13
评论0 1.7k浏览

上篇文章介绍了,只需通过实现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&lt;t&gt; : Pool&lt;t&gt; where T : IPoolAble

OK,第一个安全问题解决了。

第二个安全问题来了,我们有可能将一个IPoolAble对象回收两次。为了解决这个问题,我们可以在SafeObjectPool维护一个已经分配过的对象容器来记录对象是否被回收过,也可以在IPoolAble对象中增加是否被回收的标记。这两种方式笔者倾向于后者,维护一个容器的成本相比只是在对象上增加标记的成本来说高太多了。

我们在IPoolAble接口上增加一个bool变量来表示对象是否被回收过。

    public interface IPoolAble
    {        
        bool IsRecycled { get; set; }
    }

接着在进行Allocate和Recycle时进行标记和拦截。

    public class SafeObjectPool&lt;t&gt; : Pool&lt;t&gt; 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&lt;t&gt; : Pool&lt;t&gt;
    {
        readonly Action&lt;t&gt; mResetMethod;

        public SimpleObjectPool(Func&lt;t&gt; factoryMethod, Action&lt;t&gt; resetMethod = null,int initCount = 0)
        {
            mFactory = new CustomObjectFactory&lt;t&gt;(factoryMethod);
            mResetMethod = resetMethod;

            for (int i = 0; i &lt; 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&lt;t&gt; : Pool&lt;t&gt; 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&lt;t&gt; : IObjectFactory&lt;t&gt;
    {
        public CustomObjectFactory(Func&lt;t&gt; factoryMethod)
        {
            mFactoryMethod = factoryMethod;
        }

        protected Func&lt;t&gt; mFactoryMethod;

        public T Create()
        {
            return mFactoryMethod();
        }
    }

CustomObjectFactory不管要创建对象的构造方法是私有的还是公有的,只要开发者有办法搞出个对象就可以。现在我们要加上限制,大部分对象是new出来的。所以我们要设计一个可以new出对象的工厂。我们叫它DefaultObjectFactory。

    public class DefaultObjectFactory&lt;t&gt; : IObjectFactory&lt;t&gt; where T : new()
    {
        public T Create()
        {
            return new T();
        }
    }

注意下对泛型T的约束:)

接下来我们在构造SafeObjectPool时,创建一个DefaultObjectFactory。

    public class SafeObjectPool&lt;t&gt; : Pool&lt;t&gt; where T : IPoolAble, new()
    {
        public SafeObjectPool()
        {
            mFactory = new DefaultObjectFactory&lt;t&gt;();
        }
          ...

注意SafeObjectPool的泛型也要加上new()的约束。

这样安全的SafeObjectPool已经完成了。

我们先测试下:

        class Msg : IPoolAble
        {
            public void OnRecycled()
            {
                Log.I("OnRecycled");
            }

            public bool IsRecycled { get; set; }
        }

        private void Start()
        {
            var msgPool = new SafeObjectPool&lt;msg&gt;();

            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 &lt; 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嘛?是时候该登场了,代码如下所示。

    /// &lt;summary&gt;
    /// Object pool.
    /// &lt;/summary&gt;
    public class SafeObjectPool&lt;t&gt; : Pool&lt;t&gt;, ISingleton where T : IPoolAble, new()
    {
        #region Singleton
        protected void OnSingletonInit()
        {
        }

        public SafeObjectPool()
        {
            mFactory = new DefaultObjectFactory&lt;t&gt;();
        }

        public static SafeObjectPool&lt;t&gt; Instance
        {
            get { return QSingletonProperty&lt;SafeObjectPool&lt;t&gt;&gt;.Instance; }
        }

        public void Dispose()
        {
            QSingletonProperty&lt;SafeObjectPool&lt;t&gt;&gt;.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&lt;msg&gt;.Instance.Allocate();
            }

            public void Recycle2Cache()
            {
                SafeObjectPool&lt;msg&gt;.Instance.Recycle(this);
            }

            #endregion
        }

贴上测试代码:

            SafeObjectPool&lt;msg&gt;.Instance.Init(100, 50);            

            Log.I("msgPool.CurCount:{0}", SafeObjectPool&lt;msg&gt;.Instance.CurCount);

            var fishOne = Msg.Allocate();

            Log.I("msgPool.CurCount:{0}", SafeObjectPool&lt;msg&gt;.Instance.CurCount);

            fishOne.Recycle2Cache();

            Log.I("msgPool.CurCount:{0}", SafeObjectPool&lt;msg&gt;.Instance.CurCount);

            for (int i = 0; i &lt; 10; i++)
            {
                Msg.Allocate();
            }

            Log.I("msgPool.CurCount:{0}", SafeObjectPool&lt;msg&gt;.Instance.CurCount);

测试结果:

OnRecycled 
OnRecycled
... x50
msgPool.CurCount:50
msgPool.CurCount:49
OnRecycled
msgPool.CurCount:50
msgPool.CurCount:40

测试结果一致,现在贴上SafeObejctPool的全部代码。这篇文章内容好多,写得我都快吐了- -。

   using System;

    /// &lt;summary&gt;
    /// I cache type.
    /// &lt;/summary&gt;
    public interface IPoolType
    {
        void Recycle2Cache();
    }

    /// &lt;summary&gt;
    /// I pool able.
    /// &lt;/summary&gt;
    public interface IPoolAble
    {
        void OnRecycled();

        bool IsRecycled { get; set; }
    }

    /// &lt;summary&gt;
    /// Count observer able.
    /// &lt;/summary&gt;
    public interface ICountObserveAble
    {
        int CurCount { get; }
    }

    /// &lt;summary&gt;
    /// Object pool.
    /// &lt;/summary&gt;
    public class SafeObjectPool&lt;t&gt; : Pool&lt;t&gt;, ISingleton where T : IPoolAble, new()
    {
        #region Singleton
        public void OnSingletonInit()
        {
        }

        protected SafeObjectPool()
        {
            mFactory = new DefaultObjectFactory&lt;t&gt;();
        }

        public static SafeObjectPool&lt;t&gt; Instance
        {
            get { return QSingletonProperty&lt;SafeObjectPool&lt;t&gt;&gt;.Instance; }
        }

        public void Dispose()
        {
            QSingletonProperty&lt;SafeObjectPool&lt;t&gt;&gt;.Dispose();
        }
        #endregion


        /// &lt;summary&gt;
        /// Init the specified maxCount and initCount.
        /// &lt;/summary&gt;
        /// Max Cache count.&lt;/param&gt;
        /// Init Cache count.&lt;/param&gt;
        public void Init(int maxCount, int initCount)
        {
            if (maxCount &gt; 0)
            {
                initCount = Math.Min(maxCount, initCount);

                mMaxCount = maxCount;
            }

            if (CurCount &lt; initCount)
            {
                for (int i = CurCount; i &lt; initCount; ++i)
                {
                    Recycle(mFactory.Create());
                }
            }
        }

        /// &lt;summary&gt;
        /// Gets or sets the max cache count.
        /// &lt;/summary&gt;
        /// &lt;value&gt;The max cache count.&lt;/value&gt;
        public int MaxCacheCount
        {
            get { return mMaxCount; }
            set
            {
                mMaxCount = value;

                if (mCacheStack != null)
                {
                    if (mMaxCount &gt; 0)
                    {
                        if (mMaxCount &lt; mCacheStack.Count)
                        {
                            int removeCount = mMaxCount - mCacheStack.Count;
                            while (removeCount &gt; 0)
                            {
                                mCacheStack.Pop();
                                --removeCount;
                            }
                        }
                    }
                }
            }
        }

        /// &lt;summary&gt;
        /// Allocate T instance.
        /// &lt;/summary&gt;
        public override T Allocate()
        {
            T result = base.Allocate();
            result.IsRecycled = false;
            return result;
        }

        /// &lt;summary&gt;
        /// Recycle the T instance
        /// &lt;/summary&gt;
        /// T.&lt;/param&gt;
        public override bool Recycle(T t)
        {
            if (t == null || t.IsRecycled)
            {
                return false;
            }

            if (mMaxCount &gt; 0)
            {
                if (mCacheStack.Count &gt;= mMaxCount)
                {
                    t.OnRecycled();
                    return false;
                }
            }

            t.IsRecycled = true;
            t.OnRecycled();
            mCacheStack.Push(t);

            return true;
        }
    }

代码实现很简单,但是要考虑很多。

总结:

  • SimpleObjectPool 适合用于项目开发,渐进式,更灵活。
  • SafeObjectPool 适合用于库级开发,更多限制,要求开发者一开始就想好,更安全。

OK,今天就到这里。

相关链接:

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

教程源码:https://github.com/liangxiegame/QFramework/tree/master/Assets/HowToWriteUnityGameFramework/

转载请注明地址:凉鞋的笔记http://liangxiegame.com/

QFramework&游戏框架搭建QQ交流群: 623597263

微信公众号:liangxiegame

input/writing/Unity游戏框架搭建

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引