在游戏开发当中,GC是一个重要的优化项,Unity厂家建议每帧别超过大概50B(大概这个数量级,我也记不清了),但是对于复杂点的游戏,各种Entity逻辑,动态结构,如果不进行优化的很难做到这个级别。下面提出一种池化思想,通过进行内存管理,来减少GC。中心思想是把释放的东西保存起来管理,要用时直接取出使用。
下面直接来看代码:
public abstract class PoolObject
{
public IObjectPool _pool;
public int InstanceId;
public bool isInPool;
public Action InitAction;
public Action ReleaseAction;
public void Release()
{
_pool?.CollectObject(this);
}
}
PoolObject是一个抽象类,程序中需要实现池化管理的类需要继承这个抽象类,类中有两个委托,分别是对象创建和释放时的回调。
public interface IObjectPool
{
void CollectObject(PoolObject poolObject);
void Clear();
}
public class ObjectPool : IObjectPool where T : PoolObject, new()
{
private static ObjectPool pool;
private static List m_PooledObjList = new List();
public static T Alloc()
{
if (pool == null)
{
pool = new ObjectPool();
ObjectPoolManager.AddPool(pool);
}
T poolObject;
if (m_PooledObjList.Count == 0)
{
poolObject = new T()
{
_pool = pool,
};
}
else
{
var count = m_PooledObjList.Count;
poolObject = m_PooledObjList[count - 1];
m_PooledObjList.RemoveAt(count - 1);
}
poolObject.isInPool = false;
poolObject.InstanceId = ObjectPoolManager.NextInstanceId;
poolObject.InitAction?.Invoke();
return poolObject;
}
public void CollectObject(PoolObject poolObject)
{
poolObject.isInPool = true;
poolObject.InstanceId = 0;
poolObject.ReleaseAction?.Invoke();
m_PooledObjList.Add((T)poolObject);
}
public void Clear()
{
m_PooledObjList.Clear();
}
}
这个是资源池,有一个静态的List会去缓存释放的对象,当需要时再取出。如果数量不够就直接创建。这块的化也可以做一些优化,比如初始化的时候就创建一定的数量。每一个PoolObject会引用到Pool,释放时会到Pool中执行相应的操作。
public struct PoolObjectRef where T : PoolObject
{
private T m_poolObject;
public int m_instanceId;
public T PoolObject{
get
{
if (m_poolObject == null || m_instanceId != m_poolObject.InstanceId)
{
return null;
}
return m_poolObject;
}
}
public PoolObjectRef(T poolObject)
{
m_poolObject = poolObject;
m_instanceId = poolObject.InstanceId;
}
public bool IsNull()
{
return m_poolObject == null || m_instanceId != m_poolObject.InstanceId;
}
public bool IsRef(T p)
{
return m_poolObject == p && m_instanceId == p.InstanceId;
}
public void ReleaseRef()
{
m_poolObject = null;
m_instanceId = 0;
}
}
这是一个资源引用的管理,你可能在某一个地方引用到一个对象,但是在一个时刻对象被释放收集到资源池中,正常语法你保持这这个引用还能去访问到释放的对象,这就影响到了这个对象使用上的生命周期,按设计他不能再被使用,于是这边利用InstanceId来判断是否还保持着原对象的引用,如果对象被释放,这边也就不再保持引用。使用之前判断一下IsNull.
public static class ObjectPoolManager
{
private static List m_poolList = new List();
public static void AddPool(IObjectPool pool)
{
m_poolList.Add(pool);
}
public static void ClearPool()
{
foreach (var pool in m_poolList)
{
pool.Clear();
}
m_poolList.Clear();
}
private static int curInstanceId;
public static int NextInstanceId
{
get
{
curInstanceId++;
return curInstanceId;
}
}
}
这部分是资源池,把所有池里的资源都存在这里,统一管理。InstanceId是给所有PoolObject的一个标识,能够用来判断是否还存在引用关系。
下面给点测试代码
//先来一个Entity类
public class Entity : PoolObject
{
private static int Index = 0;
public int CreatedIndex;
public Entity()
{
InitAction = () =>
{
Console.WriteLine("Entity Inited");
};
ReleaseAction = () =>
{
Console.WriteLine("Entity Released");
};
Index++;
CreatedIndex = Index;
Console.WriteLine($"{Index} Entity Created");
}
}
Test1:
var list = new List();
for (var index = 0; index < 10; index++)
{
list.Add(ObjectPool.Alloc());
}
for (var index = 9; index >= 5; index--)
{
list[index].Release();
list.RemoveAt(index);
}
for (var index = 0; index < 10; index++)
{
list.Add(ObjectPool.Alloc());
}
先从资源池里申请10个Entity,再释放5个,再申请10个,我们可以看到Entity总共被创建了15个,说明资源池起作用了。不然这种情况Entity是会创建20次。
Test2:
var entityList = new List();
for (var i = 0; i < 15; i++) entityList.Add(ObjectPool.Alloc());
var entityRefList1 = new List>();
for (var i = 0; i < 10; i++) entityRefList1.Add(new PoolObjectRef(entityList[i]));
var entityRefList2 = new List>();
for (var i = 5; i < 15; i++) entityRefList2.Add(new PoolObjectRef(entityList[i]));
foreach (var entityRef in entityRefList1)
{
if (!entityRef.IsNull())
{
if (entityRef.PoolObject is Entity entity) Console.WriteLine($"EntityRef Object Index {entity.CreatedIndex}");
}
else
{
Console.WriteLine($"EntityRef Object Is Null");
}
}
for (int i = 12; i >= 3; i--)
{
entityList[i].Release();
entityList.RemoveAt(i);
}
foreach (var entityRef in entityRefList1)
{
if (!entityRef.IsNull())
{
if (entityRef.PoolObject is Entity entity) Console.WriteLine($"EntityRef Object Index {entity.CreatedIndex}");
}
else
{
Console.WriteLine($"EntityRef Object Is Null");
}
}
foreach (var entityRef in entityRefList2)
{
if (!entityRef.IsNull())
{
if (entityRef.PoolObject is Entity entity) Console.WriteLine($"EntityRef Object Index {entity.CreatedIndex}");
}
else
{
Console.WriteLine($"EntityRef Object Is Null");
}
}
第二个测试用例中试了下PoolObjectRef的作用。我们先创建15个Entity的数组,再创建两个PoolObjectRef的数组,一个引用前10个Entity,一个引用后10个,遍历第一个ref数组,能正常打印出所有所有数据,当我们把所以3-12的Entity放回资源池中,我们可以再遍历两个ref数组,发现第一个数组只有前3个还保持着引用,后一个数组只有最后两个还保持引用。和预期相符合。
总结:利用资源池能将程序中的GC降低到一个很低的程度,当然还有一些其它手段来降低GC。这种思想的副作用是会增大程序的内存使用,这就需要权衡一下了,看看项目对内存敏感不。
有问题或者优化思路的可以一起讨论一下。