Unity MyFramework 用法说明(十):使用 ClassObject 和 ClassScope 管理可复用对象

上一篇介绍 EventSystem 时,带参数事件通过 ClassScope 创建:

using var a = new ClassScope<EventLocationChanged>(out var param);

ClassScope 背后使用的是 MyFramework 的 ClassPool。它不仅能减少高频创建临时对象产生的 GC,也为普通 C# 对象提供了一套统一的创建、重置和销毁流程。

项目地址:

github.com/ZHOURUIH/My…

一、为什么需要 ClassPool

游戏运行过程中会频繁创建各种临时对象,例如:

事件参数
技能参数
子弹参数
网络消息
状态机参数
伤害数字数据
临时查询结果

直接使用 new 创建这些对象:

EventLocationChanged param = new EventLocationChanged();

使用结束后只能等待 GC 回收。

当创建频率较高时,大量短生命周期对象会持续产生托管内存分配,最终引起 GC。

MyFramework 的处理方式是:

从 ClassPool 获取对象
        ↓
使用对象
        ↓
将对象归还 ClassPool
        ↓
下次继续复用

要加入这套对象池体系,类需要继承 ClassObject


二、创建一个 ClassObject

以项目中的拍卖物品数据为例:

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 后,所有字段都应该在这里重置:

public override void resetProperty()
{
base.resetProperty();

mTarget = null;
mCount = 0;
mPosition = Vector3.zero;
mFinished = false;
}

遗漏任何字段,都可能导致复用对象时读取到旧数据。


三、使用 CLASS 创建长期对象

通过 CLASS<T>() 从对象池获取对象:

using static FrameUtility;

AuctionItem item = CLASS<AuctionItem>();
item.setInstanceID(instanceID);
item.setTime(timeMinute);
item.setPrice(price);
item.setAveragePrice(averagePrice);

实际项目中的 AuctionSystem 会把拍卖物品长期保存在字典中:

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 回收:

public void destroyAuction(long instanceID)
{
mAuctionList.Remove(
instanceID,
out AuctionItem auction);

UN_CLASS(ref auction);
}

推荐传入 ref

UN_CLASS(ref auction);

回收完成后,框架会同时把当前变量设置为 null,减少继续误用该引用的可能性。

对象回收过程大致为:

标记对象正在销毁
        ↓
调用 destroy()
        ↓
调用 resetProperty()
        ↓
放回未使用队列

下一次调用 CLASS<AuctionItem>() 时,就可以直接取出这个对象继续使用。


五、回收对象集合

如果一个列表中的元素全部来自 ClassPool,可以统一回收:

protected List<AuctionItem> mAuctionList = new();

public override void destroy()
{
UN_CLASS_LIST(mAuctionList);

base.destroy();
}

UN_CLASS_LIST 会:

逐个回收列表中的 ClassObject
清空列表

它支持常见集合:

List<T>
HashSet<T>
Dictionary<TKey, TValue>
Queue<T>

例如销毁全部子弹:

public void destroyAllBullet()
{
UN_CLASS_LIST(mBulletList.getMainList());
mBulletList.clear();
}

注意,集合本身不会因此变成 null,只是其中的对象被回收并清空。


六、使用 ClassScope 创建临时对象

有些对象只在当前代码块中临时使用,例如事件参数:

public class EventLocationChanged : GameEvent
{
public Vector2Int mLastTilePosition;
public Vector2Int mTilePosition;

public override void resetProperty()
{
base.resetProperty();

mLastTilePosition = Vector2Int.zero;
mTilePosition = Vector2Int.zero;
}
}

发送事件时使用 ClassScope

using var a = new ClassScope<EventLocationChanged>(
out var param);

param.mLastTilePosition = lastTilePosition;
param.mTilePosition = newTilePosition;

mEventSystem.pushEvent(param);

离开当前作用域时,ClassScope.Dispose() 会自动将对象归还对象池。

完整流程是:

进入作用域
    ↓
从 ClassPool 获取 EventLocationChanged
    ↓
填写事件参数并发送
    ↓
离开作用域
    ↓
自动归还对象池

不需要再手动调用:

UN_CLASS(ref param);

对于明确只在当前作用域使用的临时对象,优先使用 ClassScope,可以避免忘记回收。


七、不能保存 ClassScope 中的对象

下面的写法是错误的:

protected EventLocationChanged mLastEvent;

public void sendEvent()
{
using var a =
new ClassScope<EventLocationChanged>(out var param);

mLastEvent = param;
mEventSystem.pushEvent(param);
}

离开 sendEvent() 后,param 已经被归还对象池。

mLastEvent 仍然保存着它的引用。这个实例随后可能被其他逻辑重新取出并修改。

因此,ClassScope 中的对象只能在当前作用域内使用:

可以立即读取
可以传给同步执行的函数
不能保存到成员变量
不能放进长期存在的集合
不能在作用域结束后继续访问

如果对象需要长期保存,应使用 CLASS<T>() 创建,并在生命周期结束时通过 UN_CLASS 回收。


八、ClassObject 的生命周期

ClassObject 主要包含三个生命周期函数:

public virtual void onCreate()
{
}

public virtual void destroy()
{
}

public virtual void resetProperty()
{
base.resetProperty();
}

它们的职责不同。

onCreate

每次对象从池中分配出来时调用:

public override void onCreate()
{
base.onCreate();

// 每次开始使用对象时执行
}

无论对象是第一次创建,还是从池中复用,都会执行。

destroy

对象归还对象池前调用:

public override void destroy()
{
// 注销事件、解除外部关联等

base.destroy();
}

适合处理具有行为意义的清理,例如:

取消事件监听
解除管理器引用
移出其他容器
释放关联资源

resetProperty

负责将所有字段恢复到默认状态:

public override void resetProperty()
{
base.resetProperty();

mOwner = null;
mID = 0;
mData = null;
}

它不负责业务逻辑,只负责保证对象下次使用时是干净的。


九、不要依赖构造函数重复初始化

对象池复用时不会再次调用构造函数。

下面的写法只能在对象第一次被 new 出来时执行:

public AuctionItem()
{
mTimeMinute = 10;
}

对象回收后重新获取,不会再次进入构造函数。

需要每次分配时执行的逻辑,应放在:

public override void onCreate()
{
base.onCreate();
}

需要清理的字段应放在:

public override void resetProperty()
{
base.resetProperty();
}

ClassPool 也不支持依赖带参数构造函数创建对象,因为对象复用时无法重新传入构造参数。


十、主线程和子线程对象池

普通接口:

CLASS<T>();
UN_CLASS(ref obj);

使用的是主线程 ClassPool,不能在子线程中调用。

子线程需要使用:

MyData data = CLASS_THREAD<MyData>();
UN_CLASS_THREAD(ref data);

主线程对象池和线程对象池彼此独立,不应该混合创建和回收。

也就是:

CLASS 创建的对象
必须由 UN_CLASS 回收

CLASS_THREAD 创建的对象
必须由 UN_CLASS_THREAD 回收

十一、CLASS 和 ClassScope 如何选择

长期保存的业务对象:

AuctionItem item = CLASS<AuctionItem>();
mAuctionList.Add(item.getInstanceID(), item);

生命周期结束时手动回收:

UN_CLASS(ref item);

只在当前代码块使用的临时对象:

using var a = new ClassScope<EventLocationChanged>(
out var param);

选择规则可以概括为:

需要跨函数或长期保存
使用 CLASS + UN_CLASS

只在当前作用域临时使用
使用 ClassScope

十二、总结

MyFramework 的 ClassPool 用法主要包括:

1. 可复用对象继承 ClassObject
2. 在 resetProperty 中重置全部字段
3. 使用 CLASS 获取长期对象
4. 使用 UN_CLASS 手动回收
5. 使用 UN_CLASS_LIST 批量回收
6. 使用 ClassScope 管理临时对象
7. 不要保存已经离开作用域的临时对象
8. 子线程使用 CLASS_THREAD

对象池不只是把 new 换成 CLASS

真正重要的是为普通对象建立清晰的生命周期:

创建或复用
    ↓
初始化并使用
    ↓
销毁业务关联
    ↓
重置全部状态
    ↓
等待下一次复用

只有正确实现创建、使用和回收流程,对象复用才能在减少 GC 的同时保持数据安全。