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 不管要创建对象的构造方法是私有的还是公有的,只要开发者有办法搞出个对象就可以。现在我们要加上限制,大部分对象是 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

更多内容

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 前言 我们知道,Java 创建一个实例的消耗是不小的,如果没有使用栈上分配和 TLAB,那么就需要使用 CAS 在...
    莫那一鲁道阅读 4,333评论 3 11
  • 一篇影评里一句话,精彩到我花了半个小时搜集作者的其他文章。 “看的出你汪洋恣肆的才华,也看的出这种才华未经梳理” ...
    心甲阅读 241评论 1 1
  • 人间冷暖 世态炎凉 矫情似乎是每天生活的必需品 像是每次菜中必须有油盐酱醋一样 时常在想 什么时候自己才可以改变呢...
    车车车66阅读 249评论 1 2
  • 我怕当我读懂生活生活却改变了未来的我 成长此间的向往,相遇,相识,相知,相守相随的幸福与欢笑/离别与惆怅/期待及失...
    安小迈阅读 230评论 0 0
  • 放完假了, 想给大家煮点好吃的, 天气冷, 想着打火锅最合适了, 吃麻辣还是咖喱味的呢? 和你商量, 选了大家都可...
    达瓦青措阅读 422评论 0 3