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

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

bash 复制代码
using var a = new ClassScope<EventLocationChanged>(out var param);

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

项目地址:

github.com/ZHOURUIH/My...

一、为什么需要 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 的同时保持数据安全。

相关推荐
SmalBox1 天前
【节点】[EvaluateSimulationDisplacement节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
Unity MyFramework 用法说明(九):使用 EventSystem 处理不确定的业务响应
unity3d
SmalBox2 天前
【节点】[EvaluateSimulationCaustics节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[EvaluateSimulationAdditionalData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_3 天前
Unity MyFramework 用法说明(八):逻辑就是对象,使用 Command 封装跨系统业务
unity3d
qiqizizzz4 天前
Unity编译提速#05 | 认识 Assembly Definition:给代码划定边界
unity3d
_zhourui_h_4 天前
Unity MyFramework 用法说明(七):使用 RedPointSystem 构建树形红点
unity3d