Skip to content

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 判断生命周期状态。