上一篇介绍 EventSystem 时,带参数事件通过 ClassScope 创建:
bash
using var a = new ClassScope<EventLocationChanged>(out var param);
ClassScope 背后使用的是 MyFramework 的 ClassPool。它不仅能减少高频创建临时对象产生的 GC,也为普通 C# 对象提供了一套统一的创建、重置和销毁流程。
项目地址:
一、为什么需要 ClassPool
游戏运行过程中会频繁创建各种临时对象,例如:
bash
事件参数
技能参数
子弹参数
网络消息
状态机参数
伤害数字数据
临时查询结果
直接使用 new 创建这些对象:
bash
EventLocationChanged param = new EventLocationChanged();
使用结束后只能等待 GC 回收。
当创建频率较高时,大量短生命周期对象会持续产生托管内存分配,最终引起 GC。
MyFramework 的处理方式是:
bash
从 ClassPool 获取对象
↓
使用对象
↓
将对象归还 ClassPool
↓
下次继续复用
要加入这套对象池体系,类需要继承 ClassObject。
二、创建一个 ClassObject
以项目中的拍卖物品数据为例:
bash
public class AuctionItem : ClassObject
{
protected long mInstanceID;
protected int mTimeMinute;
protected int mPrice;
protected int mAveragePrice;
public void setInstanceID(long instanceID) { mInstanceID = instanceID; }
public void setTime(int timeMinute) { mTimeMinute = timeMinute; }
public void setPrice(int price) { mPrice = price; }
public void setAveragePrice(int price) { mAveragePrice = price; }
public long getInstanceID() { return mInstanceID; }
public int getTime() { return mTimeMinute; }
public int getPrice() { return mPrice; }
public int getAveragePrice() { return mAveragePrice; }
public override void resetProperty()
{
base.resetProperty();
mInstanceID = 0;
mTimeMinute = 0;
mPrice = 0;
mAveragePrice = 0;
}
}
resetProperty() 非常重要。
对象归还对象池时,框架会调用它清理数据。下次重新取出同一个实例时,不应该保留上一次使用的状态。
所以继承 ClassObject 后,所有字段都应该在这里重置:
bash
public override void resetProperty()
{
base.resetProperty();
mTarget = null;
mCount = 0;
mPosition = Vector3.zero;
mFinished = false;
}
遗漏任何字段,都可能导致复用对象时读取到旧数据。
三、使用 CLASS 创建长期对象
通过 CLASS<T>() 从对象池获取对象:
bash
using static FrameUtility;
AuctionItem item = CLASS<AuctionItem>();
item.setInstanceID(instanceID);
item.setTime(timeMinute);
item.setPrice(price);
item.setAveragePrice(averagePrice);
实际项目中的 AuctionSystem 会把拍卖物品长期保存在字典中:
bash
using System.Collections.Generic;
using static FrameUtility;
public class AuctionSystem : FrameSystem
{
protected Dictionary<long, AuctionItem> mAuctionList = new();
public AuctionItem createAuctionItem(
long instanceID,
int timeMinute,
int price,
int averagePrice)
{
if (!mAuctionList.getOrAddClass(
instanceID,
out AuctionItem item))
{
item.setInstanceID(instanceID);
item.setTime(timeMinute);
item.setPrice(price);
item.setAveragePrice(averagePrice);
}
return item;
}
}
CLASS<T>() 创建的是需要手动管理生命周期的对象。
只要业务仍然持有这个对象,它就不会自动归还对象池。
四、使用 UN_CLASS 回收对象
拍卖物品失效后,通过 UN_CLASS 回收:
bash
public void destroyAuction(long instanceID)
{
mAuctionList.Remove(
instanceID,
out AuctionItem auction);
UN_CLASS(ref auction);
}
推荐传入 ref:
bash
UN_CLASS(ref auction);
回收完成后,框架会同时把当前变量设置为 null,减少继续误用该引用的可能性。
对象回收过程大致为:
bash
标记对象正在销毁
↓
调用 destroy()
↓
调用 resetProperty()
↓
放回未使用队列
下一次调用 CLASS<AuctionItem>() 时,就可以直接取出这个对象继续使用。
五、回收对象集合
如果一个列表中的元素全部来自 ClassPool,可以统一回收:
bash
protected List<AuctionItem> mAuctionList = new();
public override void destroy()
{
UN_CLASS_LIST(mAuctionList);
base.destroy();
}
UN_CLASS_LIST 会:
bash
逐个回收列表中的 ClassObject
清空列表
它支持常见集合:
bash
List<T>
HashSet<T>
Dictionary<TKey, TValue>
Queue<T>
例如销毁全部子弹:
bash
public void destroyAllBullet()
{
UN_CLASS_LIST(mBulletList.getMainList());
mBulletList.clear();
}
注意,集合本身不会因此变成 null,只是其中的对象被回收并清空。
六、使用 ClassScope 创建临时对象
有些对象只在当前代码块中临时使用,例如事件参数:
bash
public class EventLocationChanged : GameEvent
{
public Vector2Int mLastTilePosition;
public Vector2Int mTilePosition;
public override void resetProperty()
{
base.resetProperty();
mLastTilePosition = Vector2Int.zero;
mTilePosition = Vector2Int.zero;
}
}
发送事件时使用 ClassScope:
bash
using var a = new ClassScope<EventLocationChanged>(
out var param);
param.mLastTilePosition = lastTilePosition;
param.mTilePosition = newTilePosition;
mEventSystem.pushEvent(param);
离开当前作用域时,ClassScope.Dispose() 会自动将对象归还对象池。
完整流程是:
bash
进入作用域
↓
从 ClassPool 获取 EventLocationChanged
↓
填写事件参数并发送
↓
离开作用域
↓
自动归还对象池
不需要再手动调用:
bash
UN_CLASS(ref param);
对于明确只在当前作用域使用的临时对象,优先使用 ClassScope,可以避免忘记回收。
七、不能保存 ClassScope 中的对象
下面的写法是错误的:
bash
protected EventLocationChanged mLastEvent;
public void sendEvent()
{
using var a =
new ClassScope<EventLocationChanged>(out var param);
mLastEvent = param;
mEventSystem.pushEvent(param);
}
离开 sendEvent() 后,param 已经被归还对象池。
但 mLastEvent 仍然保存着它的引用。这个实例随后可能被其他逻辑重新取出并修改。
因此,ClassScope 中的对象只能在当前作用域内使用:
bash
可以立即读取
可以传给同步执行的函数
不能保存到成员变量
不能放进长期存在的集合
不能在作用域结束后继续访问
如果对象需要长期保存,应使用 CLASS<T>() 创建,并在生命周期结束时通过 UN_CLASS 回收。
八、ClassObject 的生命周期
ClassObject 主要包含三个生命周期函数:
bash
public virtual void onCreate()
{
}
public virtual void destroy()
{
}
public virtual void resetProperty()
{
base.resetProperty();
}
它们的职责不同。
onCreate
每次对象从池中分配出来时调用:
bash
public override void onCreate()
{
base.onCreate();
// 每次开始使用对象时执行
}
无论对象是第一次创建,还是从池中复用,都会执行。
destroy
对象归还对象池前调用:
bash
public override void destroy()
{
// 注销事件、解除外部关联等
base.destroy();
}
适合处理具有行为意义的清理,例如:
bash
取消事件监听
解除管理器引用
移出其他容器
释放关联资源
resetProperty
负责将所有字段恢复到默认状态:
bash
public override void resetProperty()
{
base.resetProperty();
mOwner = null;
mID = 0;
mData = null;
}
它不负责业务逻辑,只负责保证对象下次使用时是干净的。
九、不要依赖构造函数重复初始化
对象池复用时不会再次调用构造函数。
下面的写法只能在对象第一次被 new 出来时执行:
bash
public AuctionItem()
{
mTimeMinute = 10;
}
对象回收后重新获取,不会再次进入构造函数。
需要每次分配时执行的逻辑,应放在:
bash
public override void onCreate()
{
base.onCreate();
}
需要清理的字段应放在:
bash
public override void resetProperty()
{
base.resetProperty();
}
ClassPool 也不支持依赖带参数构造函数创建对象,因为对象复用时无法重新传入构造参数。
十、主线程和子线程对象池
普通接口:
bash
CLASS<T>();
UN_CLASS(ref obj);
使用的是主线程 ClassPool,不能在子线程中调用。
子线程需要使用:
bash
MyData data = CLASS_THREAD<MyData>();
UN_CLASS_THREAD(ref data);
主线程对象池和线程对象池彼此独立,不应该混合创建和回收。
也就是:
bash
CLASS 创建的对象
必须由 UN_CLASS 回收
CLASS_THREAD 创建的对象
必须由 UN_CLASS_THREAD 回收
十一、CLASS 和 ClassScope 如何选择
长期保存的业务对象:
bash
AuctionItem item = CLASS<AuctionItem>();
mAuctionList.Add(item.getInstanceID(), item);
生命周期结束时手动回收:
bash
UN_CLASS(ref item);
只在当前代码块使用的临时对象:
bash
using var a = new ClassScope<EventLocationChanged>(
out var param);
选择规则可以概括为:
bash
需要跨函数或长期保存
使用 CLASS + UN_CLASS
只在当前作用域临时使用
使用 ClassScope
十二、总结
MyFramework 的 ClassPool 用法主要包括:
markdown
1. 可复用对象继承 ClassObject
2. 在 resetProperty 中重置全部字段
3. 使用 CLASS 获取长期对象
4. 使用 UN_CLASS 手动回收
5. 使用 UN_CLASS_LIST 批量回收
6. 使用 ClassScope 管理临时对象
7. 不要保存已经离开作用域的临时对象
8. 子线程使用 CLASS_THREAD
对象池不只是把 new 换成 CLASS。
真正重要的是为普通对象建立清晰的生命周期:
bash
创建或复用
↓
初始化并使用
↓
销毁业务关联
↓
重置全部状态
↓
等待下一次复用
只有正确实现创建、使用和回收流程,对象复用才能在减少 GC 的同时保持数据安全。