MyFramework:Unity ClassScope 如何自动回收临时对象

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

ClassScope<T> 是 MyFramework 里一个很小的结构体。

它的作用很明确:

复制代码
从 ClassPool 申请一个临时对象
离开 using 作用域时自动回收

它解决的不是对象池本身,而是对象池使用时最常见的问题:

复制代码
申请了临时对象
但忘记归还

一、代码

ClassScope<T> 的实现如下:

复制代码
using System;
using static FrameBaseHotFix;

// 用于自动从对象池中获取一个T,不再使用时会自动释放,需要搭配using来使用,比如using(new ClassScope<T>(out var value))
public struct ClassScope<T> : IDisposable where T : ClassObject, new()
{
	private T mValue;	// 分配的对象
	public ClassScope(out T value)
	{
		if (mClassPool == null)
		{
			value = new();
			mValue = null;
			return;
		}
		value = mClassPool?.newClass<T>(true);
		mValue = value;
	}
	public void Dispose()
	{
		mClassPool?.destroyClass(ref mValue);
	}
}

使用方式:

复制代码
using var a = new ClassScope<SerializerRead>(out var reader);
reader.init(data);

作用域结束时,Dispose() 会自动执行:

复制代码
mClassPool?.destroyClass(ref mValue);

对象会被归还到 ClassPool


二、临时对象

ClassScope<T> 适合生命周期很短的对象。

例如:

复制代码
SerializerRead
SerializerWrite
SerializerBitRead
SerializerBitWrite
GameEvent
临时 LayoutInfo
临时解析对象

这类对象通常只在一个函数内使用。

函数结束后,就应该回收到对象池。

如果每次都手动写:

复制代码
SerializerRead reader = mClassPool.newClass<SerializerRead>(true);

// 使用 reader

mClassPool.destroyClass(ref reader);

很容易在中间 return、异常、分支逻辑中漏掉回收。

ClassScope<T> 把这个过程收敛到 using


三、onlyOnce

ClassScope<T> 内部申请对象时使用:

复制代码
value = mClassPool?.newClass<T>(true);

这里传入的是 true

在 MyFramework 的 ClassPool 中,这表示临时对象。

临时对象会进入编辑器下的 mInusedList

复制代码
var inuseList = onlyOnce ? mInusedList : mPersistentInuseList;

如果临时对象申请后没有及时归还,ClassPool.update() 会在编辑器下报错:

复制代码
logError("有临时对象正在使用中,是否在申请后忘记回收到池中! \n" + stack + ", type:" + itemList.GetType());

所以 ClassScope<T> 不只是方便写法。

它还配合 ClassPool 的临时对象检查,减少忘记回收的问题。


四、Dispose 回收

ClassScope<T> 是结构体,并实现 IDisposable

所以可以使用:

复制代码
using var a = new ClassScope<T>(out var value);

编译后等价于在作用域结束时调用:

复制代码
a.Dispose();

Dispose() 中执行:

复制代码
mClassPool?.destroyClass(ref mValue);

destroyClass() 会完成对象池回收流程:

复制代码
外部引用置空
设置 PendingDestroy
调用 destroy()
移出使用列表
调用 resetProperty()
放回未使用队列

对象不是简单丢回队列。

回收前会走完整生命周期。


五、resetProperty

池化对象最终会执行:

复制代码
temp.resetProperty();

这一步很关键。

临时对象复用时,不能带着上一次使用的数据。

例如 SerializerRead 上一次读过一段数据,如果回收前不清理,下次复用就可能残留旧状态。

ClassScope<T> 自动回收对象。

ClassPool 自动调用 resetProperty()

两者配合后,临时对象的使用方式比较固定:

复制代码
申请
使用
作用域结束
destroy
resetProperty
入池

六、mClassPool 为空

构造函数里有一段特殊处理:

复制代码
if (mClassPool == null)
{
	value = new();
	mValue = null;
	return;
}

这表示框架对象池还没有初始化时,也能正常创建对象。

但这种情况下,mValuenull

后续 Dispose() 不会回收到对象池。

这适合框架初始化早期或单元测试场景。

对象池存在时,走池化流程。

对象池不存在时,退化成普通 new


七、ClassScope2

MyFramework 里还有 ClassScope2<T>

复制代码
public struct ClassScope2<T> : IDisposable where T : ClassObject, new()
{
	private T mValue0;  // 分配的对象
	private T mValue1;	// 分配的对象
	public ClassScope2(out T value0, out T value1)
	{
		if (mClassPool == null)
		{
			value0 = new();
			value1 = new();
			mValue0 = null;
			mValue1 = null;
			return;
		}
		value0 = mClassPool?.newClass<T>(true);
		value1 = mClassPool?.newClass<T>(true);
		mValue0 = value0;
		mValue1 = value1;
	}
	public void Dispose()
	{
		mClassPool?.destroyClass(ref mValue0);
		mClassPool?.destroyClass(ref mValue1);
	}
}

它一次申请两个同类型临时对象。

适合需要两个临时对象配合使用的场景。

逻辑和 ClassScope<T> 一样:

复制代码
构造时申请
Dispose 时回收

八、使用场景

MyFramework 中可以看到很多类似用法:

复制代码
using var a = new ClassScope<SerializerRead>(out var reader);

using var a = new ClassScope<SerializerBitWrite>(out var writer);

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

这些场景共同点是:

复制代码
对象只在当前函数内使用
使用频率可能较高
不希望频繁 new
不希望手动写 destroyClass

例如事件对象:

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

事件分发结束后,事件对象自动回收。

调用者不用在每个分支里手动释放。


九、和手动回收的区别

手动回收写法:

复制代码
mClassPool.newClass(out SerializerRead reader, true);

// 使用 reader

mClassPool.destroyClass(ref reader);

问题在于回收依赖人为记住。

如果中间出现:

复制代码
return;

或者:

复制代码
throw;

或者复杂分支:

复制代码
if (...)
{
	return;
}

就容易漏。

ClassScope<T> 写法:

复制代码
using var a = new ClassScope<SerializerRead>(out var reader);

// 使用 reader

作用域结束自动回收。

代码更短,生命周期也更明确。


十、设计边界

ClassScope<T> 适合临时对象。

不适合长期持有对象。

例如下面这种用法不合适:

复制代码
using var a = new ClassScope<MyObject>(out var obj);
mField = obj;

using 结束后,obj 会被回收到对象池。

mField 会持有一个已经失效的对象。

所以 ClassScope<T> 的边界很明确:

复制代码
对象只在当前作用域内使用
不能把对象保存到外部长期持有
不能跨帧使用
不能交给异步回调后继续使用

如果对象需要长期存在,就应该用普通 newClass<T>(false) 或其他生命周期管理方式。


十一、设计价值

ClassScope<T> 的价值不在复杂。

它只是把对象池临时对象的生命周期固定下来:

复制代码
using 开始
    从 ClassPool 取对象

using 结束
    自动 destroyClass

这带来几个效果:

复制代码
减少手动回收代码
减少忘记归还对象
减少临时对象 GC
配合 onlyOnce 做泄漏检查
配合 resetProperty 清理状态

它适合 MyFramework 中大量短生命周期对象。


总结

ClassScope<T> 的核心实现很短:

复制代码
public ClassScope(out T value)
{
	value = mClassPool?.newClass<T>(true);
	mValue = value;
}

public void Dispose()
{
	mClassPool?.destroyClass(ref mValue);
}

它利用 using 管理对象池临时对象。

申请时从 ClassPool 获取。

离开作用域时自动归还。

回收时走 destroy()resetProperty()

这个设计让临时对象的使用方式更接近 C++ 里的作用域生命周期管理。

在 Unity 项目中,它适合序列化、事件、临时解析对象等高频临时对象场景。