主题
DisposableObject 生命周期与扩展
DisposableObject 是 WasiCore 中用于管理“需要明确结束生命周期”的运行时对象基类。它把托管资源、非托管资源、宿主级联销毁和最终器兜底统一到一套 Dispose 模式中。
它不是所有 C# 类都必须继承的基类。只有当对象持有需要主动释放的资源,或者需要和框架的对象生命周期对齐时,才应该使用它。
它解决什么问题
一个运行时对象可能同时持有:
- 其他
IDisposable对象、事件订阅、计时器和临时子对象等托管资源; SceneGraph、Node、视图对象、原生句柄或其他引擎侧资源等非托管资源;- 需要从场景、宿主集合或运行时索引中注销的引用。
如果只把这些字段设为 null,并不会触发对应的清理逻辑。DisposableObject 提供统一的 Destroy() 入口,并通过 DisposeManaged()、DisposeUnmanaged(bool) 将不同类型的清理分开。
基本生命周期
| 入口 | 用途 | hostDisposedValue |
|---|---|---|
Destroy() | 普通业务逻辑主动结束对象生命周期 | false |
DestroyFromHost() | 宿主正在级联销毁时结束对象生命周期 | true |
Destroy(bool isHostDispose) | 旧程序集兼容入口;新代码不应直接使用 | 按参数设置 |
Dispose() | 标准 IDisposable 入口;不会表达宿主级联语义 | 保留当前值 |
普通销毁:Destroy()
普通游戏逻辑需要主动移除一个对象时,调用无参版本:
csharp
temporaryEffect.Destroy();
unit.Destroy();
customGame.Destroy();它表示对象独立结束生命周期。框架会执行托管清理,并在对象类型适用时从所属场景、宿主集合和运行时索引中移除对象。
宿主级销毁:DestroyFromHost()
DestroyFromHost() 的访问级别是 protected internal。框架内部可以调用;自定义 DisposableObject、Component 或 Unit 派生类在确认宿主正在级联销毁时也可以调用。
典型场景是整个 Scene 卸载:底层场景会统一销毁单位和实体的非托管部分,子对象不应再次从场景中移除或重复释放底层句柄。
调用 DestroyFromHost() 时,框架会先设置:
csharp
hostDisposedValue = true;然后才进入正常的 Dispose() 流程。因此自定义类可以在销毁重写中读取这个受保护标记,判断宿主是否已经接管级联清理。
csharp
protected override void DisposeUnmanaged(bool finalizing)
{
if (!finalizing && !hostDisposedValue)
{
// 只有对象独立销毁时,才执行需要从宿主或场景中主动摘除的逻辑。
RemoveExternalBinding();
}
ReleaseNativeResource();
base.DisposeUnmanaged(finalizing);
}hostDisposedValue 表示“当前销毁是否由宿主级联触发”,不是对象是否有效,也不是宿主已经完成全部清理的承诺。它只适合用于选择清理路径或执行宿主销毁专用逻辑。
旧的 bool 重载
Destroy(bool isHostDispose) 保留是为了兼容已经编译的旧程序集。旧代码中的:
csharp
unit.Destroy();可能在编译结果中实际调用 Destroy(bool) 并传入 false,因此不能直接删除这个元数据签名。
新代码应使用:
Destroy():普通主动销毁;DestroyFromHost():确认宿主正在级联销毁。
不要把 Destroy(true) 当作“强制删除对象”。对仍处于活动场景中的 Unit 误传 true,可能让对象已经进入 Dispose 状态,但仍被场景集合或索引持有。
重写哪些方法
DisposeManaged()
这里处理显式销毁时仍然可以安全访问的托管资源,例如:
- 取消事件订阅;
- 销毁由当前对象独立拥有的子对象;
- 清空集合、缓存和托管句柄;
- 从托管宿主集合中注销自己。
csharp
protected override void DisposeManaged()
{
OnChanged -= HandleChanged;
_child?.Destroy();
_child = null;
base.DisposeManaged();
}如果子对象属于宿主统一管理的集合,宿主销毁路径中应根据 hostDisposedValue 选择 DestroyFromHost() 或跳过重复注销,不要无条件把所有子对象当作独立对象处理。
DisposeUnmanaged(bool finalizing)
这里处理引擎对象、原生句柄和其他非托管资源。该方法既可能由显式 Dispose() 调用,也可能由最终器调用:
finalizing == false:可以执行显式销毁路径;finalizing == true:只执行不依赖其他托管对象的非托管兜底释放。
csharp
protected override void DisposeUnmanaged(bool finalizing)
{
if (_nativeHandle != IntPtr.Zero)
{
NativeApi.Release(_nativeHandle);
_nativeHandle = IntPtr.Zero;
}
base.DisposeUnmanaged(finalizing);
}不要把必须取消的托管事件、托管子对象销毁或复杂宿主访问推迟到最终器路径。
继承 DisposableObject 可以做什么
自定义运行时对象
适合封装由游戏逻辑创建、并需要在游戏结束或场景切换时释放的资源:
csharp
public sealed class RuntimeEffectController : DisposableObject
{
private SceneGraph? _sceneGraph;
private Node? _rootNode;
public void Initialize()
{
_sceneGraph = new SceneGraph(createOctree: true);
_rootNode = _sceneGraph.CreateChild("RuntimeEffectRoot");
}
protected override void DisposeManaged()
{
// SceneGraph 自己会级联销毁其 Node 和 Component。
_sceneGraph?.Dispose();
_sceneGraph = null;
_rootNode = null;
base.DisposeManaged();
}
}游戏结束时调用 controller.Destroy(),不要只丢弃 C# 引用。
自定义 Unit / Component 子类
Unit、Entity 和 Component 已经拥有框架自己的销毁逻辑。下面只展示销毁重写,构造函数和数编创建接线省略;实际自定义 Unit 仍需按 EntityComponentDataPattern 完成对应的创建接线。
csharp
public class CustomUnit : Unit
{
protected override void DisposeManaged()
{
StopCustomSubscriptions();
base.DisposeManaged();
}
protected override void DisposeUnmanaged(bool finalizing)
{
if (!finalizing && hostDisposedValue)
{
// Scene/宿主会统一处理与场景绑定的底层对象,这里不要重复摘除。
}
base.DisposeUnmanaged(finalizing);
}
}只有自定义类明确知道宿主正在级联销毁时,才应在自己的生命周期代码中调用 DestroyFromHost()。普通业务代码主动删除单位仍然调用 unit.Destroy()。
使用已有子类
很多框架类型已经间接继承 DisposableObject,不需要重复包装:
| 类型 | 典型职责 | 扩展建议 |
|---|---|---|
Entity / Unit | 场景注册、视图对象、组件和附加对象 | 在自定义销毁重写中读取 hostDisposedValue,保留基类清理 |
Component | 从实体组件集合注销、释放组件资源 | 独立移除用 Destroy(),宿主销毁用 DestroyFromHost() |
AttachedObject / Mover | 从 Unit 的宿主集合注销、清理附加逻辑 | 不要在宿主级联时重复移除宿主集合 |
Actor | 客户端表现对象生命周期 | 注意 Actor.Destroy 的 forceNoFade 参数与宿主销毁语义不同 |
StatusBarCustom 等运行时句柄 | UI 或表现资源释放 | 不复用时调用 Destroy() |
与 Dispose() 的关系
Destroy() 最终会进入 Dispose(),并且销毁操作具有防重入保护:重复调用不会重复执行释放流程。通常仍建议使用语义更明确的 Destroy():
- 它表达的是“运行时对象结束生命周期”;
- 它会明确设置普通销毁的宿主标记;
- 宿主级联场景可以选择
DestroyFromHost()。
直接调用 Dispose() 适合本身就是标准 IDisposable 的独立对象,或者某个 API 明确要求使用 Dispose() 的场景图对象。对于框架的 Entity、Unit、Component 和其他可销毁运行时对象,优先遵循各系统文档的 Destroy() 约定。
使用检查表
- 普通主动销毁:调用
Destroy()。 - 宿主正在级联销毁:在框架或自定义派生类中调用
DestroyFromHost()。 - 需要区分宿主路径:在销毁重写中读取
hostDisposedValue。 - 自定义重写:同时考虑
DisposeManaged()、DisposeUnmanaged(bool finalizing)和基类调用。 - 不要依赖最终器释放托管资源。
- 不要对活动
Unit误用Destroy(true)。 - 不要因为对象仍有 C# 引用就认为它仍然有效,应通过
IsValid/IsDisposed判断生命周期状态。