对象池管理系统PoolManager

背景

  在游戏运行过程中,频繁创建和销毁对象会产生较高的性能开销,特别是在使用大量相同类型的对象时,如子弹、特效、敌人、UI小控价等,内存临界时还会自动触发gc,造成卡顿现象。我们引入对象池技术来解决这个问题,对象池通过预先创建一组对象并重复利用它们,从而减少对象的创建和销毁操作,提升内存和性能效率。
  为了统一管理游戏中的对象池,在对象池的基础上设计了对象池管理系统PoolManager。此外,该系统能够定期清理长期闲置的对象,进一步优化对象池的内存占用。

使用

创建对象的生成函数

  我们需要给PoolManager提供目标对象的生成方法,即 对象生成函数 ,告诉PoolManager这个对象应该怎么生成,以及生成时的初始化、事件通知等其他自定义操作。
  下面以一个窗口 Windows 装填多个相同ui控件 Item 的需求举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Windows : MonoBehaviour
{
private List<Item> _list = new List<Item>(); // itme对象列表

/// <summary>
/// Item的创建函数,对象生成函数: System.Func<out T>
/// </summary>
/// <returns>返回创建的item实例化对象</returns>
private Item ItemGeneratorFunc()
{
var obj = new GameObject();
var item = obj.AddComponent<Item>();
item.Init(); // 初始化item
return item;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 对象池对象需要继承IPoolableObject接口,并实现接口函数
public class Item : MonoBehaviour, IPoolableObject
{
// 实现接口函数
public float LastUsedTime { get; } // 对象上一次使用的时间,交由PoolManager进行自动销毁算法判断
public bool Active { get; } // 是否激活中,OnActivate()和OnDeactivate()会进行修改
public void OnActivate(){} // 激活时
public void OnDeactivate(){} // 主动归还时
public void OnIdleDestroy(){} // PoolManager自动销毁对象时

// ……其他需求逻辑
public Image image;
// 初始化
public void Init(){}
// 刷新表现
public void Refresh(){}
}

申请对象池

向PoolManager申请创建一个对象池,PoolManager会为Item对象生成一个对应的对象池,该对象池全局唯一,处处可向PoolManager获取到

1
2
3
4
5
6
public class Windows : MonoBehaviour
{
// 向PoolManager申请创建Item对象池
// PoolManager.Instance.CreatePool(初始对象数量, 对象生成函数Func);
PoolManager.Instance.CreatePool(4, ItemGeneratorFunc);
}

获取对象

任意地方可以获取对象,GetObject() 获取对象时,对象会自动执行OnActivate()

1
2
3
4
5
6
7
8
9
10
// 获取10个item
for (var i = 0; i < 10; i++)
{
// PoolManager.Instance.GetObject<对象类>()
var item = PoolManager.Instance.GetObject<Item>();

// ……其他自定义逻辑
item.Refresh();
_list.Add(item);// 装填item对象
}

归还对象

ReturnObject() 归还对象时,对象会自动执行OnDeactivate()

1
2
3
4
5
6
7
8
// windows窗口销毁时,归还item对象
private void OnDestroy()
{
foreach (var item in _list)
{
PoolManager.Instance.ReturnObject(item);
}
}

为了避免忘记归还对象,造成内存泄漏,建议Item对象销毁时主动归还

1
2
3
4
5
6
7
public class Item : MonoBehaviour, IPoolableObject
{
public void OnDestroy()
{
PoolManager.Instance.ReturnObject(this);
}
}

注意事项

  • 重复归还对象时会提示warring,说明代码写的有问题,虽然PoolManager处理了重复归还,但就规范来说,应该保证只归还一次对象,开发者应当自行修正归还逻辑。
  • 由于对象池内部实现逻辑的抽象程度较高,避免开发者自定义创建 继承对象池 时出现预期外的错误,对象池管理系统虽然允许开发者自定义对象池,但是PoolManager中取消了添加自定义对象池的函数接口。如果开发者对自定义对象池的需求较强烈,可以深入理解对象池管理机制,然后自行在PoolManager中实现 添加继承对象池 的函数接口,或者在具体对象内部维护私有的该继承对象池(不建议)。

内部实现

  • PoolManager使用了面向接口编程的方式,使得内部 维护不同类型对象的对象池列表 变得可行且简洁。
    1
    2
    3
    4
    // 声明对象池接口
    public interface IObjectPool
    {
    }
    1
    2
    3
    4
    // 具体对象池,继承对象池接口
    public class ObjectPool<T> : IObjectPool where T : IPoolableObject
    {
    }
    1
    2
    3
    4
    5
    6
    public class PoolManager
    {
    // PoolManager维护 IObjectPool 列表,不需要关心对象池 ObjectPool<T> 的 具体对象类型 T
    // 达到不同类型对象的对象池,能共存于同一个字典中的目的
    private Dictionary<Type, IObjectPool> _pools;
    }
  • 优化对象池内存占用,传统对象池在归还对象后,并不作另外处理,简而言之就是只增加对象不销毁对象。这样会导致对象池的内存占用越来越大,最终导致内存优化效率还不如不用对象池。PoolManager使用指数衰减算法,以较低频率对所有对象池的所有闲置对象进行扫描,并以一定比例销毁闲置超时的闲置对象,同时保持指定比例的对象留存量。向gc机制标记该闲置对象可清除,以便在手动gc时进行内存归还。使得各对象池的内存占用维持在较低水平。逻辑详见 Yu.ObjectPool.AutoDestroy()
  • 为了达到任意位置可以获取对象池或者获取对象的目的,设计成单例模式,并交由GameManager管理声明周期:
    1
    2
    3
    4
    public class PoolManager:BaseSingleTon<PoolManager>,IMonoManager
    {
    // ……实现 IMonoManager 接口函数
    }