对象池
全库目录221
本文目录8
弹丸反复创建销毁,可以改成租用已有实例,但上一轮速度、任务和事件必须先清理。对象池管理的是取得与释放契约,槽位复用之前还要确认旧使用者退出。
租用与复用
弹丸释放时如果只隐藏模型,旧的速度、拖尾和事件订阅仍然存在。下一次从池里取出同一实例,新的使用者就会继承上一轮运动,旧命中回调还可能再次扣伤害。因此池管理的是一次租用从开始到结束的状态,空闲表只是保存可再次出租的槽位。
Free → Acquiring → Active → Retiring → Free取得槽位后先用本次上下文重置所有者、位置、速度、计时器和表现,再开放任务与事件。释放则先关闭入口,取消订阅与任务,等已经开始的访问结束,再清空临时引用、物理状态和拖尾历史。只有最后一步完成,槽位才重新进入空闲表。获得取消信号的任务仍可能在退出途中,因此不能把“发出取消”当成“所有读者已结束”。
租用与生命周期回调
Unity对脚本实例只调用一次 Awake。实例化后对象在层级中处于激活状态时会触发它;实例化非激活对象时,调用可延后到首次激活。此后的启用和禁用按组件及对象状态触发 OnEnable/OnDisable,不会重复执行 Awake。若业务把每次使用初始化放在 Awake,对象池后就会出错。
建议定义显式 OnAcquire(context) / OnRelease(),让每次租用状态可测试。引擎生命周期只负责底层接入。
句柄与释放竞争
一次租用用(slot,generation)标识。槽位7的第12代弹丸结束后进入Retiring,旧命中回调仍带第12代句柄。槽位以后出租为第13代时,代数比较拒绝旧回调修改新弹丸。但仅先比较再解锁也不够:比较通过之后若另一线程释放并出租,后续访问仍落在新实例上。因此解析句柄、取得借用资格与确认Active必须位于同一保护范围,借用计数在实际访问结束后才归还。
下面是带锁池的机制伪代码。保护区内只修改元数据,取消、回调和等待在锁外执行;StopAndWait要求最终返回时全部旧借用已经结束。
Release(handle):
with poolLock:
entry = entries[handle.slot]
if entry.generation != handle.generation: return stale
if entry.state != Active: return already_released_or_busy
entry.state = Retiring // 此后拒绝新借用和第二次Release
StopAndWait(entry) // 停入口、取消订阅/任务,等待已有借用结束
ResetPayload(entry) // 清理速度、拖尾与临时引用
with poolLock:
assert entry.state == Retiring and entry.borrowCount == 0
entry.state = Free
freeList.push(handle.slot)Free转Acquiring时也在同一把锁内独占槽位并递增代数;初始化成功才转Active,失败则清理到Free。两个Release即使同时到达,也只有第一个能把Active改成Retiring,第二个不会再次插入空闲表。StopAndWait失败时保持Retiring并报告失败,不能提前出租。代数解决身份,保护范围与等待解决访问期间,清理负责下一次初始状态,三者分别对应不同失效。
容量与驻留成本
容量可按最大同时活跃数量加余量估计,预热只减少首次创建成本。记录活跃峰值、空闲驻留和溢出次数;若对象大部分时间闲置且保留大量贴图引用,池会把临时峰值变成长驻内存。对象池的收益由完整租还周期测量,不能只比较一次new与一次取槽。
结构或Prefab版本改变时,空闲实例仍携带旧组件与资源,应按版本销毁或重建,不能仅重置几个字段就假定兼容。容量耗尽时可拒绝、临时创建或按玩法显式回收;回收仍在使用的槽位必须经过同一Retiring流程。
验证方法
测试重复释放、陈旧句柄、容量耗尽与场景切换,观察事件和轨迹是否残留。比较预热、稳态分配和长期驻留,只有高频重复工作才可能抵消池管理成本。
实现与证据
正文为对象池状态契约与伪代码。已检查陈旧代数、重复归还与等待条件;未运行Unity激活回调或池化性能测量。
相关主题
参考资料
-
Unity6 MonoBehaviour.Awake:非激活对象首次激活和每实例一次的调用条件;2026-10-22核读。
-
Unity6 ObjectPool<T>:池接口、取回与释放回调;具体生命周期由项目契约定义。