Article

C# 实战课 14:资源与生命周期治理(长期运行不炸内存)

系列定位:Unity 老版本兼容(.NET 3.5 / C# 4)+ 项目驱动。
本章目标:把“能跑”升级到“能长时间稳定跑”,让战斗 30~60 分钟连续运行仍可控。

学习目标

完成本章后,你应该能做到:

  1. 给核心对象定义清晰生命周期边界(创建、启用、停用、销毁)。
  2. 识别并修复常见泄漏源:事件未解绑、定时器未取消、池对象脏状态。
  3. 建立统一的资源注册表与销毁顺序。
  4. 输出可量化指标:对象存活数、池命中率、每分钟 GC 次数。

背景问题

战斗系统在前 13 章已经可用,但一旦长时间运行会出现:

  1. 内存缓慢上涨,最终卡顿或崩溃。
  2. 场景切换后“幽灵回调”继续触发。
  3. 对象池复用后出现旧状态污染。
  4. 回放/重开后行为异常,无法复现。

根因不是某一行代码,而是缺少统一生命周期治理模型

生命周期分层模型

建议把对象分成 3 层:

  1. Session 级:一场战斗内存活(如 BattleWorld、SystemManager)。
  2. Entity 级:随角色/子弹生成销毁(Actor、Projectile)。
  3. Frame 级:每帧临时对象(命中列表、排序缓存、事件缓冲)。

治理原则:

  • Session 级只能在 Match Begin/End 边界创建与清理。
  • Entity 级优先池化,禁止频繁 new/destroy
  • Frame 级必须复用容器,不允许泄漏到下一帧。

核心接口:可治理生命周期

public interface ILifecycle
{
    void OnCreate();
    void OnEnable();
    void OnDisable();
    void OnDestroy();
}
public sealed class LifecycleHost
{
    private readonly List<ILifecycle> _items = new List<ILifecycle>(64);

    public void Register(ILifecycle item)
    {
        if (item == null)
        {
            return;
        }

        _items.Add(item);
        item.OnCreate();
        item.OnEnable();
    }

    public void DisableAll()
    {
        for (var i = _items.Count - 1; i >= 0; i--)
        {
            _items[i].OnDisable();
        }
    }

    public void DestroyAll()
    {
        for (var i = _items.Count - 1; i >= 0; i--)
        {
            _items[i].OnDestroy();
        }

        _items.Clear();
    }
}

倒序销毁是关键:避免上层已销毁而下层还在回调。

项目驱动:战斗模块治理落地

1. 事件订阅自动解绑

public sealed class EventSubscriptionGroup : ILifecycle
{
    private readonly EventBus _eventBus;
    private readonly List<string> _keys = new List<string>(16);
    private readonly List<Action<object>> _handlers = new List<Action<object>>(16);

    public EventSubscriptionGroup(EventBus eventBus)
    {
        _eventBus = eventBus;
    }

    public void Bind(string key, Action<object> handler)
    {
        _eventBus.Subscribe(key, handler);
        _keys.Add(key);
        _handlers.Add(handler);
    }

    public void OnCreate() { }
    public void OnEnable() { }

    public void OnDisable()
    {
        for (var i = 0; i < _keys.Count; i++)
        {
            _eventBus.Unsubscribe(_keys[i], _handlers[i]);
        }
    }

    public void OnDestroy()
    {
        _keys.Clear();
        _handlers.Clear();
    }
}

2. 调度器任务令牌统一回收

public sealed class SchedulerTokenGroup : ILifecycle
{
    private readonly Scheduler _scheduler;
    private readonly List<int> _tokens = new List<int>(32);

    public SchedulerTokenGroup(Scheduler scheduler)
    {
        _scheduler = scheduler;
    }

    public void Add(int token)
    {
        if (token > 0)
        {
            _tokens.Add(token);
        }
    }

    public void OnCreate() { }
    public void OnEnable() { }

    public void OnDisable()
    {
        for (var i = 0; i < _tokens.Count; i++)
        {
            _scheduler.Cancel(_tokens[i]);
        }

        _tokens.Clear();
    }

    public void OnDestroy() { }
}

3. 池对象脏状态防线

public interface IPoolResettable
{
    void ResetForPool();
}

public sealed class BulletRuntime : IPoolResettable
{
    public int OwnerId;
    public int TargetId;
    public float Speed;
    public bool Active;

    public void ResetForPool()
    {
        OwnerId = 0;
        TargetId = 0;
        Speed = 0f;
        Active = false;
    }
}
public sealed class SafePool<T> where T : class, IPoolResettable
{
    private readonly ObjectPool<T> _inner;

    public SafePool(Func<T> factory, int initialCapacity)
    {
        _inner = new ObjectPool<T>(factory, null, delegate(T x) { x.ResetForPool(); }, initialCapacity);
    }

    public T Get()
    {
        return _inner.Get();
    }

    public void Release(T value)
    {
        _inner.Release(value);
    }
}

资源注册表(场景级总控)

public sealed class BattleResourceRegistry : ILifecycle
{
    public readonly LifecycleHost LifecycleHost;
    public readonly EventSubscriptionGroup Subscriptions;
    public readonly SchedulerTokenGroup SchedulerTokens;

    public BattleResourceRegistry(EventBus eventBus, Scheduler scheduler)
    {
        LifecycleHost = new LifecycleHost();
        Subscriptions = new EventSubscriptionGroup(eventBus);
        SchedulerTokens = new SchedulerTokenGroup(scheduler);
    }

    public void OnCreate()
    {
        LifecycleHost.Register(Subscriptions);
        LifecycleHost.Register(SchedulerTokens);
    }

    public void OnEnable() { }

    public void OnDisable()
    {
        LifecycleHost.DisableAll();
    }

    public void OnDestroy()
    {
        LifecycleHost.DestroyAll();
    }
}

这样场景结束时只做一次:

_registry.OnDisable();
_registry.OnDestroy();

运行时监控指标

最低限度建议每 5 秒输出一次:

  1. alive_actor_count
  2. pool_inactive_bullet
  3. pending_scheduler_tasks
  4. gc_collections_gen0
  5. event_subscription_count

示例:

FoundationLog.Info(
    "Health",
    "actors=" + actorCount +
    " bulletPoolInactive=" + bulletPool.CountInactive +
    " schedulerPending=" + schedulerPending +
    " gc0=" + GC.CollectionCount(0)
);

回归验收

  1. 30 分钟连续战斗后,内存曲线稳定在可控区间。
  2. 场景切换 20 次,无幽灵事件回调。
  3. 回放模式与实时模式切换 10 次,无状态污染。
  4. 对象池命中率达到预期(例如 > 90%)。

常见坑

坑 1:在 OnDestroy 才解绑事件

对象停用后仍会收事件,导致无效逻辑执行。应该在 OnDisable 即解绑。

坑 2:池对象只重置一半字段

最容易引发“偶现 Bug”。必须建立统一 ResetForPool() 协议。

坑 3:清理顺序错误

先销毁 EventBus,再解绑订阅会触发异常。总是先解绑,再销毁总线。

本月作业

实现“战斗健康检查面板”:

  1. 实时展示上述 5 个运行指标。
  2. 指标超过阈值自动告警并落日志。
  3. 一键导出最近 60 秒监控快照与回放文件路径。

下一章进入 C# 路线收官阶段:工程化测试与回归基线,把前面所有基础设施纳入可自动验证流程。