引用池的作用

GF这里的引用池其实就是我们平常说的对象池,在程序中主要是起到防止对象被频繁创建和销毁、减少gc、预加载等作用,若对对象池作用和原理不太清楚的话,可以参考书籍《游戏编程模式》中对象池模式一节。
GF中池子有两种,一种叫引用池,一种叫对象池,两者原理一样,但具体实现和针对的对象不同,引用池一般用来储存普通的C#类型对象,而对象池则一般用于储存UnityEngine下的对象(如Unity中的GameObject对象),两者区别将在下一篇对象池篇中详细分析。本文将详细说一下其中的引用池。

引用池的实现

结构

引用池部分主要由4个部分组成,静态类ReferencePool、ReferencePool的内部类ReferenceCollection、结构体ReferencePoolInfo和接口IReference。

IReference接口

IReference接口只包含一个Clear方法,此方法会在对象回收池被调用,每一个需要被引用池储存的类型都需要实现此接口,以能清空当前状态,恢复到初始状态,供下次使用。

ReferenceCollection池子类

游戏中对象池通常不止一个,对象池应该为每个需要用到对象池的类型,都创建一个对象池,不同类型的对象储存在各自类型的池子中。这里一个ReferenceCollection对象则代表了一个类型的引用池。

  • ReferenceCollection内部用m_References字段(Queue类型)来储存池子中的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public T Acquire<T>() where T : class, IReference, new()
{
if (typeof(T) != m_ReferenceType)
{
throw new GameFrameworkException("Type is invalid.");
}

m_UsingReferenceCount++;
m_AcquireReferenceCount++;
lock (m_References)
{
if (m_References.Count > 0)
{
return (T)m_References.Dequeue();
}
}

m_AddReferenceCount++;
return new T();
}

  • Acquire方法获取池子中的一个对象,若当前池子中存在可用对象,则直接从队列取出,若当前池子没有可用对象则会通过反射创建新对象并返回,注意这里新创建的对象是不会直接放进池子中的,只有当用户把他放回池子的时候才会加入储存队列中。Acquire泛型方法要求泛型参数是class、实现了IReference接口,且具有公共的无参数构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void Release(IReference reference)
{
reference.Clear();
lock (m_References)
{
if (m_EnableStrictCheck && m_References.Contains(reference))
{
throw new GameFrameworkException("The reference has been released.");
}

m_References.Enqueue(reference);
}

m_ReleaseReferenceCount++;
m_UsingReferenceCount--;
}
  • 回收对象时需要调用Release方法,把对象作为参数传进来,方法内部会把对象加入到可用队列中。
  • Add和Remove方法可以按数量直接往池子队列里添加或移除对象,RemoveAll则清空引用池,这几个接口主要用于预加载和确保后续一定时间内不会用到这些对象时,在性能不敏感期间手动释放引用池。
  • 属性ReferenceType表示该引用池持有对象的类型。
  • 属性UnusedReferenceCount、UsingReferenceCount、AcquireReferenceCount、ReleaseReferenceCount、AddReferenceCount、RemoveReferenceCount,分别表示目前池子可用的数量(剩余可被取出的数量)、被取出未归还的数量、请求获取的次数、释放的次数、实际实例化次数、主动移除次数。这些数据会在调用引用池相应接口时进行计算,外部可获取这些数据进行Debug。

ReferencePool类

ReferencePool是一个静态类,负责管理所有类型的引用池,也是外部访问引用池的入口,注意,上面介绍的ReferenceCollection类,其实是ReferencePool类的私有内部类,外部不会直接访问ReferenceCollection,而是通过访问静态类ReferencePool的API,ReferencePool内部再获取对应类型的ReferenceCollection进行相应的操作。

  • ReferencePool包含一个Dictionary<Type, ReferenceCollection>类型字段s_ReferenceCollections,用于储存所有引用池实例,类内通过私有方法GetReferenceCollection向s_ReferenceCollections获取某个类型的引用池实例,若s_ReferenceCollections不存在该类型,则构造一个,为惰性初始化。
  • 属性Count可以获取引用池数量。
  • 其中Acquire、Release、Add、Remove、RemoveAll都是通过GetReferenceCollection获取到对应类型的引用池实例ReferenceCollection后,调用该实例的相应方法。
  • ClearAll方法可以清空并销毁所有引用池。
  • GetAllReferencePoolInfos方法可以获取所有引用池的数据,返回值是ReferencePoolInfo数组类型,具体数据其实来自于ReferenceCollection类的属性当中,这些数据主要用于外部对引用池Debug。注意ReferencePoolInfo是个结构体,本身不会造成gc负担,但数组是会有gc的。

强制类型检查

特别地说一下,ReferencePool类内有个bool类型变量EnableStrictCheck,控制着类型检测开关,若处于True状态时会在部分对引用池操作的步骤中,加入类型检测的步骤。

ReferencePool类的InternalCheckReferenceType方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void InternalCheckReferenceType(Type referenceType)
{
if (!m_EnableStrictCheck)
{
return;
}

if (referenceType == null)
{
throw new GameFrameworkException("Reference type is invalid.");
}

if (!referenceType.IsClass || referenceType.IsAbstract)
{
throw new GameFrameworkException("Reference type is not a non-abstract class type.");
}

if (!typeof(IReference).IsAssignableFrom(referenceType))
{
throw new GameFrameworkException(Utility.Text.Format("Reference type '{0}' is invalid.", referenceType.FullName));
}
}

ReferenceCollection类中的Release方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void Release(IReference reference)
{
reference.Clear();
lock (m_References)
{
if (m_EnableStrictCheck && m_References.Contains(reference))
{
throw new GameFrameworkException("The reference has been released.");
}

m_References.Enqueue(reference);
}

m_ReleaseReferenceCount++;
m_UsingReferenceCount--;
}

根据上面代码我们可以观察出,开启类型检测,一方面检测是不是非抽象Class,且实现了IReference接口的Class,另一方面是释放对象(也就是把对象放回对象池时),需要检查这个对象是不是已经在池子中了,如果业务逻辑有误,同一个对象,重复放回池子的话,那整个池子的状态就被破坏了,下次取出时可能会发生奇怪且难以定位的bug(如取出某个对象,在使用期间突然状态被Clear了)。

示例

引用池以本系列文章的有限状态机篇的示例作为扩展,把状态类实例作为复用对象,这样如果我们重复销毁、创建状态机持有者实例时,可以复用这些状态实例。
可到有限状态机篇参看原本未使用引用池时的实现。主要修改是IdleState和MoveState实现IReference接口的Move方法,并添加静态方法Create,作用是向引用池获取一个自身类型的对象。Player类则注意调用状态类的Create方法来获取实例,并且在Player销毁时释放状态实例。

空闲状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using UnityEngine;
using GameFramework.Fsm;
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
using UnityGameFramework.Runtime;
using GameFramework;

public class IdleState : FsmState<Player>, IReference
{
//触发移动的指令列表
private static KeyCode[] MOVE_COMMANDS = { KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow };

protected override void OnInit(ProcedureOwner fsm)
{
base.OnInit(fsm);
}

protected override void OnEnter(ProcedureOwner fsm)
{
base.OnEnter(fsm);
}

protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);

foreach (var command in MOVE_COMMANDS)
{
//触发任何一个移动指令时
if (Input.GetKeyDown(command))
{
//记录这个移动指令
fsm.SetData<VarInt32>("MoveCommand", (int)command);
//切换到移动状态
ChangeState<MoveState>(fsm);
}
}
}

protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
{
base.OnLeave(fsm, isShutdown);
}

protected override void OnDestroy(ProcedureOwner fsm)
{
base.OnDestroy(fsm);
}

public static IdleState Create()
{
IdleState state = ReferencePool.Acquire<IdleState>();
return state;
}

public void Clear()
{
//此类无状态记录,Clear为空实现
}
}

移动状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using UnityEngine;
using GameFramework.Fsm;
using ProcedureOwner = GameFramework.Fsm.IFsm<Player>;
using UnityGameFramework.Runtime;
using GameFramework;


public class MoveState : FsmState<Player>, IReference
{
private static readonly float EXIT_TIME = 1f;
private float exitTimer;
private KeyCode moveCommand;

protected override void OnInit(ProcedureOwner fsm)
{
base.OnInit(fsm);
}

protected override void OnEnter(ProcedureOwner fsm)
{
base.OnEnter(fsm);

//进入移动状态时,获取移动指令数据
moveCommand = (KeyCode)(int)fsm.GetData<VarInt32>("MoveCommand");
}

protected override void OnUpdate(ProcedureOwner fsm, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);

//计时器累计时间
exitTimer += elapseSeconds;

//switch(moveCommand)
//{
//根据移动方向指令向对应方向移动
//}

//达到指定时间后
if (exitTimer > EXIT_TIME)
{
//切换回空闲状态
ChangeState<IdleState>(fsm);
}
}

protected override void OnLeave(ProcedureOwner fsm, bool isShutdown)
{
base.OnLeave(fsm, isShutdown);

//推出移动状态时,把计时器清零
exitTimer = 0;
//清空移动指令
moveCommand = KeyCode.None;
fsm.RemoveData("MoveCommand");
}

protected override void OnDestroy(ProcedureOwner fsm)
{
base.OnDestroy(fsm);
}

public static MoveState Create()
{
MoveState state = ReferencePool.Acquire<MoveState>();
return state;
}

public void Clear()
{
//还原状态内数据
exitTimer = 0;
moveCommand = KeyCode.None;
}
}

玩家类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System.Collections.Generic;
using UnityEngine;
using GameFramework.Fsm;
using StarForce;
using GameFramework;

public class Player : MonoBehaviour
{
private static int SERIAL_ID = 0;

private IFsm<Player> fsm;

// Start is called before the first frame update
void Start()
{
//创建状态列表(不用引用池)
//List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };

//创建状态列表(使用引用池)
List<FsmState<Player>> stateList = new List<FsmState<Player>>() { IdleState.Create(), MoveState.Create() };

//创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复
fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);
//以IdleState为初始状态,启动状态机
fsm.Start<IdleState>();
}

// Update is called once per frame
void Update()
{

}

private void OnDestroy()
{
//取出状态机所有状态
FsmState<Player>[] states = fsm.GetAllStates();
//销毁状态机
GameEntry.Fsm.DestroyFsm(fsm);

//把状态实例归还引用池
foreach (var item in states)
{
ReferencePool.Release((IReference)item);
}
}
}

Inspector面板

可以通过引用池的Inspector面板Enable Strick Check来开启上文说的类型检测,面板上还会具体显示当前引用池数量,并按程序集分开显示所有类型的引用池的ReferencePoolInfo信息。可以通过此面板方便地检查业务逻辑中有没有正确使用引用池,例如某个对象只会在某个流程中会使用,我们可以检测在流程循环中,这个对象的Acquire和Release是否相等,而流程结束时,Using是否为0,Unused是否与Add相等。

思考

为什么需要类型检查

上面已经提及到,类型检查一方面检查是不是非抽象Class,且实现了IReference接口的Class,另一方面是释放对象(也就是把对象放回对象池时),需要检查这个对象是不是已经在池子中了。我们再来看看下面ReferencePool的一段源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// 从引用池获取引用。
/// </summary>
/// <typeparam name="T">引用类型。</typeparam>
/// <returns>引用。</returns>
public static T Acquire<T>() where T : class, IReference, new()
{
return GetReferenceCollection(typeof(T)).Acquire<T>();
}

/// <summary>
/// 从引用池获取引用。
/// </summary>
/// <param name="referenceType">引用类型。</param>
/// <returns>引用。</returns>
public static IReference Acquire(Type referenceType)
{
InternalCheckReferenceType(referenceType);
return GetReferenceCollection(referenceType).Acquire();
}

可以看到当用泛型时是不需要调用InternalCheckReferenceType的,只有用非泛型时才需要调用InternalCheckReferenceType来检查,其实是因为泛型通过where来进行约束了,如果使用者以错误的方式使用的话是会产生编译错误的,但如果用参数类型为Type的重载的话,内部会使用反射来创建实例,这样无法像泛型那样具备编译时的类型安全特性,所以需要先一步检查类型,若错误则抛出错误,中断逻辑,避免后续实例化失败破坏了引用池的状态。

什么时候开启类型检查

类型检查也是基于反射的,对性能会造成一定影响,特别是我们使用引用池一般都是针对高频复用的实例,这样造成的性能损耗并不符合我们的要求。实际上开启类型检查后,启动框架也会在Console出现提示“Strict checking is enabled for the Reference Pool. It will drastically affect the performance.“。
对此,作者E大建议是仅在测试环境下开启,ReferencePool在Inspector面板直接提供了AlwaysEnable、OnlyEnableWhenDevelopment、OnlyEnableInEditor、AlwaysDisable 4种模式可选,分别对应 总是启用、仅在开发模式时启用、仅在编辑器中启用、总是禁用,大家在使用时可以根据需求选择。

最后

GameFramework解析 系列目录:GameFramework解析:开篇

个人原创,未经授权,谢绝转载!