13.Unity Zenject高级编程(MemoryPools内存池)

示例

使用Unity开发游戏使用适当的内存管理非常重要,如果你想制作流畅的游戏在手机上面运行;根据运行的平台和制作的游戏类型不同,对于尽可能的避免开辟不需要的堆内存非常的重要;最有效的途径的使用内存池,代码示例

public class Foo
{
    public class Factory : PlaceholderFactory<Foo>
    {
    }
}

public class Bar
{
    readonly Foo.Factory _fooFactory;
    readonly List<Foo> _foos = new List<Foo>();

    public Bar(Foo.Factory fooFactory)
    {
        _fooFactory = fooFactory;
    }

    public void AddFoo()
    {
        _foos.Add(_fooFactory.Create());
    }

    public void RemoveFoo()
    {
        _foos.RemoveAt(0);
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();
        Container.BindFactory<Foo, Foo.Factory>();
    }
}

这里,每次我们调用Bar.AddFoo将会开辟新的堆内存;每次我们调用Bar.RemoveFoo,Bar类将会从Foo的实例里面停止引用,因此Foo的实例内存标记为可以垃圾回收;如果这样发生的次数过多,最终垃圾回收器将变成负担并且你的游戏由于
spike(急剧上升)过高导致卡顿,使用memory pools来修复急剧上升的开销替代

public class Foo
{
    public class Pool : MemoryPool<Foo>
    {
    }
}

public class Bar
{
    readonly Foo.Pool _fooPool;
    readonly List<Foo> _foos = new List<Foo>();

    public Bar(Foo.Pool fooPool)
    {
        _fooPool = fooPool;
    }

    public void AddFoo()
    {
        _foos.Add(_fooPool.Spawn());
    }

    public void RemoveFoo()
    {
        var foo = _foos[0];
        _fooPool.Despawn(foo);
        _foos.Remove(foo);
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();
        Container.BindMemoryPool<Foo, Foo.Pool>();
    }
}

正如你看到的这样,内存池和工厂非常相似,除了部分的术语一点不同之后,不像工厂你需要返回实例给池子相比工厂的
仅仅是停止引用实例
使用了这种新的实现方式,将会在调用AddFoo()初始化的时候分配堆内存,但你调用RemoveFoo()然后再AddFoo(),池子
序列将重用之前的实例并因此节省你的堆内存开销
这样更好,我们想避免因为初始化导致内存的急剧上升;解决方法是再游戏启动的时候初始化好所有的内存开销

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();
        Container.BindMemoryPool<Foo, Foo.Pool>().WithInitialSize(10);
    }
}

当我们使用WithInitialSize语句绑定语法到我的池子,10个实例化的Foo将会在启动的时候立即创建作为池子的种子

Binding Syntax 绑定语法

内存池的语法大多数和工厂一样,只有部分新的绑定方法,类如 WithInitialSize 和 ExpandBy;同样,不像BindFactory,
没有必要给工厂指定参数,BindMemoryPool生成参数
和工厂一样,推荐使用内部公开类命名为Pool

public class Foo
{
    public class Pool : MemoryPool<Foo>
    {
    }
}

通过添加生成参数来设置参数

public class Foo
{
    public class Pool : MemoryPool<string, int, Foo>
    {
    }
}

全部的绑定语法格式

Container.BindMemoryPool<ObjectType, MemoryPoolType>()
    .With(InitialSize|FixedSize)
    .WithMaxSize(MaxSize)
    .ExpandBy(OneAtATime|Doubling)()
    .WithFactoryArguments(Factory Arguments)
    .To<ResultType>()
    .WithId(Identifier)
    .FromConstructionMethod()
    .AsScope()
    .WithArguments(Arguments)
    .OnInstantiated(InstantiatedCallback)
    .When(Condition)
    .CopyIntoAllSubContainers()
    .NonLazy();

字段说明:

  • InitialSize - 这个值决定启动的时候播种多少个对象到内存池里面;对于避免游戏期间急剧上升的内存有帮助
  • FixedSize - 设置这个值,内存池将会初始化指定的数量种子,如果这个值超过了,则抛异常
  • MaxSize - 设置这个值,如果有足够的对象已经返回到池子里面,超过了内存池的最大数量,多余的对象将会被销毁;
    这个决定内存池最大开辟的内存空间
  • ObjectType - 通过内存池实例化的类类型
  • MemoryPoolType - MemoryPool派生类的类型,通常是一个内部类命名为Pool
  • ExpandBy - 当内存池达到最大数量的时候决定内存池的怎么进行拓展,注意当使用WithFixedSize的时候,这个选项无效
  1. ExpandByOneAtATime - 当有需求的时候,一次只多开辟一个新对象
  2. ExpandByDoubling - 当内存池满了并且有一个新实例请求,内存池将开辟双倍的容量在返回请求实例之前;这个方法在需要开辟数量多,功能小的对象非常有帮助
  • WithFactoryArguments - 如果你想注入额外的参数到内存池的派生类,你可以通过这里传递,注意WithArguments是给实例传递参数而不是内存池传递参数
  • Scope - 注意和正常的绑定不一样,默认是AsCached替代AsTransient
    剩下的绑定方法和正常的绑定方法一样

Resetting Items In Pool 重置池子对象

我们要注意使用内存池替代工厂必须确保完全重置了指定的实例;这个是非常有必要的,有可能上一个状态来之之前的数 据,重新实例化之后还保留了原来的数据,内存池的实现方法有下列这些

public class Foo
{
    public class Pool : MemoryPool<Foo>
    {
        protected override void OnCreated(Foo item)
        {
            // Called immediately after the item is first added to the pool
        }

        protected override void OnDestroyed(Foo item)
        {
            // Called immediately after the item is removed from the pool without also being spawned
            // This occurs when the pool is shrunk either by using WithMaxSize or by explicitly shrinking the pool by calling the `ShrinkBy` / `Resize methods
        }

        protected override void OnSpawned(Foo item)
        {
            // Called immediately after the item is removed from the pool
        }

        protected override void OnDespawned(Foo item)
        {
            // Called immediately after the item is returned to the pool
        }

        protected override void Reinitialize(Foo foo)
        {
            // Similar to OnSpawned
            // Called immediately after the item is removed from the pool
            // This method will also contain any parameters that are passed along
            // to the memory pool from the spawning code
        }
    }
}

大多数情况下,你大概只要实现Reinitialize方法;代码示例

public class Foo
{
    Vector3 _position = Vector3.zero;

    public void Move(Vector3 delta)
    {
        _position += delta;
    }

    public class Pool : MemoryPool<Foo>
    {
        protected override void Reinitialize(Foo foo)
        {
            foo._position = Vector3.zero;
        }
    }
}

注意我们的池子可以自由访问Foo的私有变量,因为池子是一个内部类;或者,我们避免在Foo和Foo.Pool里面重复出现

public class Foo
{
    Vector3 _position;

    public Foo()
    {
        Reset();
    }

    public void Move(Vector3 delta)
    {
        _position += delta;
    }

    void Reset()
    {
        _position = Vector3.zero;
    }

    public class Pool : MemoryPool<Foo>
    {
        protected override void Reinitialize(Foo foo)
        {
            foo.Reset();
        }
    }
}

运行时传递参数

和工厂一样,你可以在运行时生成新的实例传递参数,不同的是,替代参数注入到类里面,通过Reinitialize方法

public class Foo
{
    Vector3 _position;
    Vector3 _velocity;

    public Foo()
    {
        Reset(Vector3.zero);
    }

    public void Tick()
    {
        _position += _velocity * Time.deltaTime;
    }

    void Reset(Vector3 velocity)
    {
        _position = Vector3.zero;
        _velocity = Vector3.zero;
    }

    public class Pool : MemoryPool<Vector3, Foo>
    {
        protected override void Reinitialize(Vector3 velocity, Foo foo)
        {
            foo.Reset(velocity);
        }
    }
}

public class Bar
{
    readonly Foo.Pool _fooPool;
    readonly List<Foo> _foos = new List<Foo>();

    public Bar(Foo.Pool fooPool)
    {
        _fooPool = fooPool;
    }

    public void AddFoo()
    {
        float maxSpeed = 10.0f;
        float minSpeed = 1.0f;

        _foos.Add(_fooPool.Spawn(
            Random.onUnitSphere * Random.Range(minSpeed, maxSpeed)));
    }

    public void RemoveFoo()
    {
        var foo = _foos[0];
        _fooPool.Despawn(foo);
        _foos.Remove(foo);
    }
}

Factories,Pools,and the Dispose Pattern

上面提供的方法可以大致运行起来,但有以下不足:

  • 每次我们设置类为内存池我们总是需要给内存池编码重置方法Reset,给方法传递必要的参数,如果自动重置
    替代我们手动给对象重置这样更简单
  • 任何代码通过池子生成的对象需要一个池子对象的主引用,方便我们调用Despawn方法;这个代码并不关心
    对象池子或者没有,实际上对象池子有更细致的实现,因此最好使用抽象隔离代码
  • 每次我们需要转换一些没有池子的对象使用池子我们需要改动大量的代码;我们需要移除PlaceholderFactory派生
    类,然后改变代码使用Spawn替代Create,也要记得调用Despawn

我们可以通过使用PlaceholderFactory和Dispose Pattern来解决这些问题;任何代码可以调用工厂的创建方法和非常内存池
对象一样并且调用Dispose自动返回对象到池子里面

public class Foo : IPoolable<IMemoryPool>, IDisposable
{
    IMemoryPool _pool;

    public void Dispose()
    {
        _pool.Despawn(this);
    }

    public void OnDespawned()
    {
        _pool = null;
    }

    public void OnSpawned(IMemoryPool pool)
    {
        _pool = pool;
    }

    public class Factory : PlaceholderFactory<Foo>
    {
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.BindFactory<Foo, Foo.Factory>().FromPoolableMemoryPool(x => x.WithInitialSize(2));
    }
}

为了完成这个,我们使用了PlaceholderFactory内部派生类和非池子对象一样,实现了IPoolable< IMemoryPool>接口;
这个将会要求我们定义OnSpawned和OnDespawned方法来处理之前我们Reset逻辑;
我们也可以实现IDisposable来返回我们自己指定池子无论Dispose是否调用
当我们绑定了工厂,我们可以使用FromPoolableMemoryPool方法来配置池子,初始化种子值,最大数量,
拓展方法来构建对象;
注意如果你遇到了IL2CPP AOT问题,你应该使得内存池类更明确

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.BindFactory<Foo, Foo.Factory>().FromPoolableMemoryPool<Foo, FooPool>(x => x.WithInitialSize(2));
    }

    public class FooPool : PoolableMemoryPool<IMemoryPool, Foo>
    {
    }
}

发生这样的问题因为有时候IL2CPP不会自动包含PoolableMemoryPool

Memory Pools for GameObjects

游戏物体的内存池和普通的内存池差不多,示例:

public class Foo : MonoBehaviour
{
    Vector3 _velocity;

    [Inject]
    public void Construct()
    {
        Reset(Vector3.zero);
    }

    public void Update()
    {
        transform.position += _velocity * Time.deltaTime;
    }

    void Reset(Vector3 velocity)
    {
        transform.position = Vector3.zero;
        _velocity = velocity;
    }

    public class Pool : MonoMemoryPool<Vector3, Foo>
    {
        protected override void Reinitialize(Vector3 velocity, Foo foo)
        {
            foo.Reset(velocity);
        }
    }
}

public class Bar
{
    readonly Foo.Pool _fooPool;
    readonly List<Foo> _foos = new List<Foo>();

    public Bar(Foo.Pool fooPool)
    {
        _fooPool = fooPool;
    }

    public void AddFoo()
    {
        float maxSpeed = 10.0f;
        float minSpeed = 1.0f;

        _foos.Add(_fooPool.Spawn(
            Random.onUnitSphere * Random.Range(minSpeed, maxSpeed)));
    }

    public void RemoveFoo()
    {
        var foo = _foos[0];
        _fooPool.Despawn(foo);
        _foos.Remove(foo);
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public GameObject FooPrefab;

    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();

        Container.BindMemoryPool<Foo, Foo.Pool>()
            .WithInitialSize(2)
            .FromComponentInNewPrefab(FooPrefab)
            .UnderTransformGroup("Foos");
    }
}

主要的不同是Foo.Pool现在从MonoMemoryPool派生而不是MemoryPool;MonoMemoryPools是一个帮助类自动enable和
disable游戏物体当我们added/removed从池子里面,MonoMemoryPool是实现代码:

public abstract class MonoMemoryPool<TParam1, TValue> : MemoryPool<TParam1, TValue>
    where TValue : Component
{
    Transform _originalParent;

    protected override void OnCreated(TValue item)
    {
        item.gameObject.SetActive(false);
        // Record the original parent which will be set to whatever is used in the UnderTransform method
        _originalParent = item.transform.parent;
    }

    protected override void OnDestroyed(TValue item)
    {
        GameObject.Destroy(item.gameObject);
    }

    protected override void OnSpawned(TValue item)
    {
        item.gameObject.SetActive(true);
    }

    protected override void OnDespawned(TValue item)
    {
        item.gameObject.SetActive(false);

        if (item.transform.parent != _originalParent)
        {
            item.transform.SetParent(_originalParent, false);
        }
    }
}

因此,如果你重写了MonoMemoryPool之中的任意一个方法,你得确保父类已经被调用base version;
注意为了确保池子的正常逻辑,MonoBehaviour应该在Prefab的跟节点上,其他情况只有节点和子节点会被disable
你也可以使用Dispose模式,和non-MonoBehaviour的做法差不多,代码:

public class Foo : MonoBehaviour, IPoolable<Vector3, IMemoryPool>, IDisposable
{
    Vector3 _velocity;
    IMemoryPool _pool;

    public void Dispose()
    {
        _pool.Despawn(this);
    }

    public void Update()
    {
        transform.position += _velocity * Time.deltaTime;
    }

    public void OnDespawned()
    {
        _pool = null;
        _velocity = Vector3.zero;
    }

    public void OnSpawned(Vector3 velocity, IMemoryPool pool)
    {
        transform.position = Vector3.zero;
        _pool = pool;
        _velocity = velocity;
    }

    public class Factory : PlaceholderFactory<Vector3, Foo>
    {
    }
}

public class Bar
{
    readonly Foo.Factory _fooFactory;
    readonly List<Foo> _foos = new List<Foo>();

    public Bar(Foo.Factory fooFactory)
    {
        _fooFactory = fooFactory;
    }

    public void AddFoo()
    {
        float maxSpeed = 10.0f;
        float minSpeed = 1.0f;

        var foo = _fooFactory.Create(
            Random.onUnitSphere * Random.Range(minSpeed, maxSpeed));

        foo.transform.SetParent(null);

        _foos.Add(foo);
    }

    public void RemoveFoo()
    {
        if (_foos.Any())
        {
            var foo = _foos[0];
            foo.Dispose();
            _foos.Remove(foo);
        }
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public GameObject FooPrefab;

    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();

        Container.BindFactory<Vector3, Foo, Foo.Factory>().FromMonoPoolableMemoryPool<Foo>(
            x => x.WithInitialSize(2).FromComponentInNewPrefab(FooPrefab).UnderTransformGroup("FooPool"));
    }
}

和上面的示例不一样,我们从PlaceholderFactory派生,实现了IDisposable接口,使用了FromMonoPoolableMemoryPool替代了FromPoolableMemoryPool,这种情况也要注意AOT错误

Static Memory Pools 静态内存池

另外i一种可能使用内存池是对安装器不进行改变,存储在类里面的静态内存池StaticMemoryPool,示例:

public class Foo
{
    public static readonly StaticMemoryPool<Foo> Pool =
        new StaticMemoryPool<Foo>(OnSpawned, OnDespawned);

    static void OnSpawned(Foo that)
    {
        // Initialize
    }

    static void OnDespawned(Foo that)
    {
        // Reset
    }
}

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var foo = Foo.Pool.Spawn();

        // Use foo

        Foo.Pool.Despawn(foo);
    }
}

在这种情况下,内存池直接作为类里面静态成员变量来访问;这种情况在对象不需要引用的时候有帮助,或者你不想总是到处安装内存池;然而,和正常的内存池不一样的事情,在池子里面的对象将会一直留在内存里面即使切换了场景,池子的清除操作通过手动调用Foo.Pool.Clear
跟上面的一样,你也可以使用Dispose一样的despawn对象,示例:

public class Foo : IDisposable
{
    public static readonly StaticMemoryPool<Foo> Pool =
        new StaticMemoryPool<Foo>(OnSpawned, OnDespawned);

    public void Dispose()
    {
        Pool.Despawn(this);
    }

    static void OnSpawned(Foo that)
    {
        // Initialize
    }

    static void OnDespawned(Foo that)
    {
        // Reset
    }
}

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var foo = Foo.Pool.Spawn();

        // Use foo

        foo.Dispose();
    }
}

你也可以引入参数到你的静态池子里面,使用StaticMemoryPool的生成参数,和正常内存池相似:

public class Foo : IDisposable
{
    public static readonly StaticMemoryPool<string, Foo> Pool =
        new StaticMemoryPool<string, Foo>(OnSpawned, OnDespawned);

    public string Value
    {
        get; private set;
    }

    public void Dispose()
    {
        Pool.Despawn(this);
    }

    static void OnSpawned(string value, Foo that)
    {
        that.Value = value;
    }

    static void OnDespawned(Foo that)
    {
        that.Value = null;
    }
}

同样,和正常的内存池一样,你可以使用IPoolable接口来结合PoolableStaticMemoryPool使用,避免样板代码和使用实例方法替代静态方法:

public class Foo : IPoolable<string>, IDisposable
{
    public static readonly PoolableStaticMemoryPool<string, Foo> Pool =
        new PoolableStaticMemoryPool<string, Foo>();

    public string Data
    {
        get; private set;
    }

    public void Dispose()
    {
        Pool.Despawn(this);
    }

    public void OnSpawned(string data)
    {
        Data = data;
    }

    public void OnDespawned()
    {
        Data = null;
    }
}

Use statements and dispose pattern

在下面的代码中有一些弊端:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var foo = Foo.Pool.Spawn();

        // Use foo

        foo.Dispose();
    }
}
  1. 我们必须总是记得调用Dispose()在方法的末尾;我们有可能更容易忘记这样做导致每帧开辟了内存
  2. 如果我们想要有多个返回值的方法,在每种情况下我们需要复制清除代码,有可能导致错误累赘
  3. 如果在Spawn和Dispose之间发生了异常,对象将不会再次返回到内存池里面
    一种简单的解决这些问题的办法是添加try-finally语句:
public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var foo = Foo.Pool.Spawn();

        try
        {
            // Use foo
        }
        finally
        {
            foo.Dispose();
        }
    }
}

等同代码,使用using语句

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var foo = Foo.Pool.Spawn())
        {
            // Use foo
        }
    }
}

这种情况下,保证Foo对象将会返回到池子里面,不管是否有异常发生或者方法过早退出;这也另外一个理由使用Dispose行为有益于构建内存池对象

List Pool 列表池子/容器池子

静态内存池子对于通用数据结构比如列表或者容器非常有帮助;Zenject包含了一些你可能额外需要的标准内存池子;例如,你编写一个MonoBehaviour需要每帧迭代每个组件,你可能会这样做:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var components = this.GetComponents(typeof(Component));

        foreach (var component in components)
        {
            // Some logic
        }
    }
}

然而,如果你在一个场景里面运行这个脚本,打开性能分析器,你将看到48bytes左右每帧开辟;我们可以最大化减少内存的开销,使用ListPool类来替代:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        var components = ListPool<Component>.Instance.Spawn();

        this.GetComponents(typeof(Component), components);

        foreach (var component in components)
        {
            // Some logic
        }

        ListPool<Component>.Instance.Despawn(components);
    }
}

Zenject还包含了DictionaryPool,HashSetPool,和ArrayPool类,使用方式差不多

Dispose Block 布置代码块

Zenject也提供了DisposeBlock类,简单的IDisposable集合对象,当使用了DisposeBlock.Dispose,集合全部disposed;
这个对于使用using语法有帮助,当你需要通过同一个池子或多个池子开辟多个临时实例;例如,我们需要spawn多个临时
对象在我们的PoolExample,可以这样做:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var foo = Foo.Pool.Spawn())
        using (var bar = Bar.Pool.Spawn())
        {
            // Some logic
        }
    }
}

这些可以正常运行,我们需要spawn更多的对象的时候,没有拓展性;因此更好的替代是使用DisposeBlock类:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var block = DisposeBlock.Spawn())
        {
            var foo = Foo.Pool.Spawn();
            var bar = Bar.Pool.Spawn();

            block.Add(foo);
            block.Add(bar);

            // Some logic
        }
    }
}

使用简化的版本,DisposeBlock.Spawn方法:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var block = DisposeBlock.Spawn())
        {
            var foo = block.Spawn(Foo.Pool);
            var bar = block.Spawn(Bar.Pool);

            // Some logic
        }
    }
}

我们也可以使用DisposeBlock来改进我们的ListPool,这样就不需要手动调用Despawn:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var block = DisposeBlock.Spawn())
        {
            var components = block.Spawn(ListPool<Component>.Instance);

            this.GetComponents(typeof(Component), components);

            foreach (var component in components)
            {
                // Some logic
            }
        }
    }
}

由于spawning lists这样的通用,DisposeBlock为此包含了一个帮助方法,因此可以这样简化:

public class PoolExample : MonoBehaviour
{
    public void Update()
    {
        using (var block = DisposeBlock.Spawn())
        {
            var components = block.SpawnList<Component>();

            this.GetComponents(typeof(Component), components);

            foreach (var component in components)
            {
                // Some logic
            }
        }
    }
}

PoolableMemoryPool

如果你宁愿不使用上面解释的布置代码块,我们也可以通过正常使用Reset方法避免样板代码,然后你可以设置通过使用
PoolableMemoryPool或者MonoPoolableMemoryPool,代码示例:

public class Foo : IPoolable<string>
{
    public string Data
    {
        get; private set;
    }

    public void OnDespawned()
    {
        Data = null;
    }

    public void OnSpawned(string data)
    {
        Data = data;
    }

    public class Pool : PoolableMemoryPool<string, Foo>
    {
    }
}

PoolableMemoryPool的实现非常简单并且只是在IPoolable类调用了实例方法:

public class PoolableMemoryPool<TParam1, TValue> : MemoryPool<TParam1, TValue>
    where TValue : IPoolable<TParam1>
{
    protected override void OnDespawned(TValue item)
    {
        item.OnDespawned();
    }

    protected override void Reinitialize(TParam1 p1, TValue item)
    {
        item.OnSpawned(p1);
    }
}

你也可以设置他方法OnSpawned和OnDespawned成私有,通过使用C#的特性“明确接口实现”,你只要允许通过IPoolable接口调用OnSpawned和OnDespawned方法

public class Foo : IPoolable<string>
{
    public string Data
    {
        get; private set;
    }

    void IPoolable<string>.OnDespawned()
    {
        Data = null;
    }

    void IPoolable<string>.OnSpawned(string data)
    {
        Data = data;
    }

    public class Pool : PoolableMemoryPool<string, Foo>
    {
    }
}

Abstract Memory Pools抽象内存池

和抽象工厂一样,有时候你可能想创建一个内存池返回一个接口,实体类型通过安装器来决定,代码示例:

public interface IFoo
{
}

public class Foo1 : IFoo
{
}

public class Foo2 : IFoo
{
}

public class FooPool : MemoryPool<IFoo>
{
}

public class Bar
{
    readonly FooPool _fooPool;
    readonly List<IFoo> _foos = new List<IFoo>();

    public Bar(FooPool fooPool)
    {
        _fooPool = fooPool;
    }

    public void AddFoo()
    {
        _foos.Add(_fooPool.Spawn());
    }

    public void RemoveFoo()
    {
        var foo = _foos[0];
        _fooPool.Despawn(foo);
        _foos.Remove(foo);
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public bool Use1;

    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();

        if (Use1)
        {
            Container.BindMemoryPool<IFoo, FooPool>().WithInitialSize(10).To<Foo1>();
        }
        else
        {
            Container.BindMemoryPool<IFoo, FooPool>().WithInitialSize(10).To<Foo2>();
        }
    }
}

我们也可能想添加Reset()方法到IFoo接口,在Reinitialize()里面调用

Subcontainers/Facades And Memory Pools

你可能好奇,如果你使用动态子容器和外观模式,什么是最好的路径确保子容器在池子里面;代码示例:

public class EnemyFacade : MonoBehaviour, IPoolable<IMemoryPool>, IDisposable
{
    IMemoryPool _pool;

    public void OnSpawned(IMemoryPool pool)
    {
        _pool = pool;
    }

    public void OnDespawned()
    {
        _pool = null;
    }

    public void Dispose()
    {
        _pool.Despawn(this);
    }

    public class Factory : PlaceholderFactory<EnemyFacade>
    {
    }
}

public class EnemyMoveHandler : MonoBehaviour
{
    Settings _settings;
    float _velocity;

    [Inject]
    public void Construct(Settings settings)
    {
        _settings = settings;
    }

    public void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            _velocity += Time.deltaTime * _settings.Acceleration;
        }

        transform.position += Vector3.forward * _velocity * Time.deltaTime;
    }

    [Serializable]
    public class Settings
    {
        public float Acceleration;
    }
}

public class Runner : ITickable
{
    readonly EnemyFacade.Factory _enemyFactory;

    EnemyFacade _enemy;

    public Runner(EnemyFacade.Factory enemyFactory)
    {
        _enemyFactory = enemyFactory;
    }

    public void Tick()
    {
        if (Input.GetKeyDown(KeyCode.Space) && _enemy == null)
        {
            _enemy = _enemyFactory.Create();
        }

        if (Input.GetKeyDown(KeyCode.Escape) && _enemy != null)
        {
            _enemy.Dispose();
            _enemy = null;
        }
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public EnemyMoveHandler.Settings EnemyMoveHandlerSettings;
    public GameObject EnemyPrefab;

    public override void InstallBindings()
    {
        Container.BindInstance(EnemyMoveHandlerSettings);
        Container.BindInterfacesTo<Runner>().AsSingle();

        Container.BindFactory<EnemyFacade, EnemyFacade.Factory>()
            .FromMonoPoolableMemoryPool<EnemyFacade>(b => b
                .WithInitialSize(1)
                .FromSubContainerResolve()
                .ByNewPrefabMethod(EnemyPrefab, InstallEnemy)
                .UnderTransformGroup("Enemies"));
    }

    void InstallEnemy(DiContainer subContainer)
    {
        subContainer.Bind<EnemyFacade>().FromNewComponentOnRoot().AsSingle();
        subContainer.Bind<EnemyMoveHandler>().FromNewComponentOnRoot().AsSingle().NonLazy();
    }
}

如果我们添加这些代码到项目里面,运行起来玩家可以按空格生产敌人实例,按ESC取消生产敌人;然后,一旦生产了,用户口语按上箭头给敌人施加加速度;
这个运行正常,有一个问题;spawn新敌人之后,添加加速度,despawn敌人,然后又是再次spawn,加速度一直没有改变;
但我们从池子里面spawn对象,我们总是想要设置到默认状态;随着项目的做大,类增多,子容器变大,都要重置
我们可以通过EnemyFacade的Reset方法修复这个,明确的在其他对象调用其他的Reset方法,但如果有更多的生成方式来做这些事情更好;替代这些,你可以添加内置的PoolableManager类到子容器里面并且在任何类需要的时候实现IPoolable接口,代码示例:

public class EnemyMoveHandler : MonoBehaviour, IPoolable
{
    Settings _settings;
    float _velocity;

    [Inject]
    public void Construct(Settings settings)
    {
        _settings = settings;
    }

    public void OnDespawned()
    {
    }

    public void OnSpawned()
    {
        _velocity = 0;
        transform.position = Vector3.zero;
    }

    public void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            _velocity += Time.deltaTime * _settings.Acceleration;
        }

        if (Input.GetKey(KeyCode.DownArrow))
        {
            _velocity -= Time.deltaTime * _settings.Acceleration;
        }

        transform.position += Vector3.forward * _velocity * Time.deltaTime;
    }

    [Serializable]
    public class Settings
    {
        public float Acceleration;
    }
}

public class EnemyFacade : MonoBehaviour, IPoolable<IMemoryPool>, IDisposable
{
    [Inject]
    PoolableManager _poolableManager;

    IMemoryPool _pool;

    public void OnSpawned(IMemoryPool pool)
    {
        _pool = pool;
        _poolableManager.TriggerOnSpawned();
    }

    public void OnDespawned()
    {
        _pool = null;
        _poolableManager.TriggerOnDespawned();
    }

    public void Dispose()
    {
        _pool.Despawn(this);
    }

    public class Factory : PlaceholderFactory<EnemyFacade>
    {
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public EnemyMoveHandler.Settings EnemyMoveHandlerSettings;
    public GameObject EnemyPrefab;

    public override void InstallBindings()
    {
        Container.BindInstance(EnemyMoveHandlerSettings);
        Container.BindInterfacesTo<Runner>().AsSingle();

        Container.BindFactory<EnemyFacade, EnemyFacade.Factory>()
            .FromMonoPoolableMemoryPool<EnemyFacade>(b => b
                .WithInitialSize(2)
                .FromSubContainerResolve()
                .ByNewPrefabMethod(EnemyPrefab, InstallEnemy)
                .UnderTransformGroup("Enemies"));
    }

    static void InstallEnemy(DiContainer subContainer)
    {
        subContainer.Bind<EnemyFacade>().FromNewComponentOnRoot().AsSingle();
        subContainer.BindInterfacesAndSelfTo<EnemyMoveHandler>().FromNewComponentOnRoot().AsSingle().NonLazy();
        subContainer.Bind<PoolableManager>().AsSingle();
    }
}

注意以下事项:

  • 我们在子容器里面放置了PoolableManger
  • 在EnemyFacade,我们必须调用了PoolableManager的TriggerOnSpawned和TriggerOnDespawned方法,触发OnSpawn
    和OnDespawned方法,重置子容器类
    注意:IPoolable类的执行顺序使用同样的执行顺序,通过BindExecutionOrder方法来设置,和其他的标准接口一样;也要注
    意的是OnDespawned相对于OnSpwaned方法反序调用

Instantiating Memory Pools Directly 直接实例化内存池

应付复杂的情况包括自定义工厂,有可能想直接实例化内存池;在这种情况下,你必须确保提供IFactory<>派生类,用于创建新实例和所有的设置信息通过绑定语法正常提供:

public class BarFactory : IFactory<Bar>
{
    public Bar Create()
    {
        ...
        [Custom creation logic]
        ...
    }
}

var settings = new MemoryPoolSettings(
    // Initial size
    1, 
    // Max size
    int.MaxValue, 
    PoolExpandMethods.Double);

var pool = _container.Instantiate<MemoryPool<Bar>>(
    new object[] { settings, new MyBarFactory<Bar>() });

Pool Cleanup Checker池子清除检查

有时候会发生当我们使用内存池生产了对象当对象有可能没有返回到池子里面;发生这样如果Spawn方法掉用了单然后程序员忘记了添加配对的Despwan/Dispose方法;
这不总是一个问题,有时候甚至故意不回收;假设生产的对象不是MonoBehaviour,一旦对象没有使用了,将会通过C#的垃圾回收自动销毁,因此忘记Despwan在这样的情况下不会产生内存泄漏;弊端是当对象没有返回到池子里面,意味着池子下次需要生产更多的对象,弱微的影响性能;
同样当然,如果你的对象有重要的自定义dispose逻辑,将会错过,这会有问题;
为了发现这些错误,Zenject包含了一个可选的类,当激活的生产对象的场景关闭了,将会抛出异常;在SceneInstaller里面添加以下代码:

Container.BindInterfacesTo<PoolCleanupChecker>().AsSingle()

这样,你每次忘记调用Despawn方法,Unity PlayMode结束后你将会在控制台获得错误报告;当池子只有0个激活的item并且在LateDispose事件中,这个类检查每个符合条件的池子;

一旦这个在一些情况下故意安排,你可以指定池子禁止错误报告,通过给PoolCleanupChecker传递一个参数类型列表:

Container.BindInterfacesTo<PoolCleanupChecker>().AsSingle()
Container.BindInstance(
    new List<Type>()
    {
        typeof(Foo.Pool),
        typeof(Bar.Pool),
    })
    .WhenInjectedInto<PoolCleanupChecker>();

Memory Pool Monitor 内存池监视器

Zenject也包含了一个体验的编辑窗口,你可以监视场景中所有内存池的大小;通过
Window -> Zenject Pool Monitor打开窗口:
13.Unity Zenject高级编程(MemoryPools内存池)_第1张图片
如果你确保你使用的池子全部都是合理的,监视器非常有作用;它也可以帮助你在初始化的时候选择一个合理的大小,在
WithInitializeSize方法确保所有的开辟内存发生在loading场景,而不是在游戏里面;
同样,你可以使用Expand按钮和Clear按钮配合Unity的新能分析器,观察池子销毁分析总计使用了多少内存

社区搭建火热进行中

Zenject Github地址
Zenject 中文社区,搭建中
讨论群:518658723
欢迎喜欢使用DI的朋友加入群,有自愿报名活动!
本人代码功底浅,大神看到就当路过,希望来点评!


呀哈哈,有能力的朋友可以扫下面的打赏二维码↓↓↓
打赏

你可能感兴趣的:(Zenject框架,Zenject,Unity,Zenject,游戏框架)