Unity项目停止运行时优雅处理OnDestroy中的单例调用
在Unity开发中,单例模式因其便捷的全局访问特性被广泛使用,但当项目停止运行或场景切换时,OnDestroy生命周期方法的执行顺序不确定性常常导致单例重复创建和报错。本文将深入探讨这一问题的根源,并提供一套健壮的解决方案。
1. 问题现象与根源分析
许多Unity开发者都遇到过这样的报错信息:Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)。这个错误并非每次都会出现,而是呈现出概率性特征,这正是问题棘手之处。
核心问题在于Unity的对象销毁机制:
OnDestroy方法的调用顺序在Unity中是不确定的- 当项目停止运行时,所有对象的
OnDestroy都会被调用,但顺序无法保证 - 如果单例A的
OnDestroy先执行,而单例B的OnDestroy后执行且又调用了单例A,就会导致单例A被重新创建
// 典型的问题代码示例 private void OnDestroy() { // 如果单例已经被销毁,这里会触发重新创建 SomeManager.Instance.DoSomething(); }2. 单例模式的实现方式对比
在Unity中,单例模式主要有三种实现方式,各有优缺点:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通C#单例 | 简单高效 | 无法继承MonoBehaviour | 纯逻辑管理类 |
| MonoBehaviour单例 | 可使用Unity生命周期 | 需处理销毁问题 | 需要Unity功能的单例 |
| DontDestroyOnLoad单例 | 跨场景持久化 | 需手动销毁 | 全局管理器 |
对于需要继承MonoBehaviour的单例,我们需要特别注意销毁时的处理,这正是本文要解决的核心问题。
3. 健壮的MonoBehaviour单例基类实现
下面是一个经过优化的单例基类实现,它解决了OnDestroy调用顺序问题:
public abstract class SafeMonoSingleton<T> : MonoBehaviour where T : MonoBehaviour { private static T _instance; private static readonly object _lock = new object(); private static bool _applicationIsQuitting = false; public static T Instance { get { if (_applicationIsQuitting) { Debug.LogWarning($"[{typeof(T)}] 实例已在应用退出时销毁,返回null"); return null; } lock (_lock) { if (_instance == null) { _instance = FindObjectOfType<T>(); if (_instance == null) { var singletonObject = new GameObject(); _instance = singletonObject.AddComponent<T>(); singletonObject.name = $"{typeof(T)} (Singleton)"; DontDestroyOnLoad(singletonObject); } } return _instance; } } } protected virtual void OnDestroy() { if (_instance == this) { _applicationIsQuitting = true; _instance = null; } } protected virtual void OnApplicationQuit() { _applicationIsQuitting = true; } }关键改进点:
- 使用
_applicationIsQuitting标志位来标记应用退出状态 - 添加线程安全锁确保多线程环境下的安全性
- 优先查找现有实例,避免不必要的创建
- 在
OnDestroy和OnApplicationQuit中都设置了退出标志
4. 实际使用中的最佳实践
基于上述基类,我们可以这样实现具体的单例管理器:
public class GameManager : SafeMonoSingleton<GameManager> { public int CurrentScore { get; private set; } public void AddScore(int points) { CurrentScore += points; Debug.Log($"当前分数: {CurrentScore}"); } protected override void OnDestroy() { base.OnDestroy(); Debug.Log("GameManager正在销毁..."); } }在OnDestroy中安全调用单例的推荐方式:
private void OnDestroy() { // 使用?.操作符进行安全调用 GameManager.Instance?.AddScore(10); // 或者显式检查 if (!GameManager.ApplicationIsQuitting && GameManager.Instance != null) { GameManager.Instance.SaveGame(); } }5. 场景切换时的特殊处理
对于需要跨场景保持的单例,我们还需要考虑场景切换时的特殊情况:
public class AudioManager : SafeMonoSingleton<AudioManager> { private AudioSource _backgroundMusic; protected override void Awake() { base.Awake(); _backgroundMusic = GetComponent<AudioSource>(); DontDestroyOnLoad(gameObject); } public void PlayMusic(AudioClip clip) { if (_backgroundMusic.isPlaying && _backgroundMusic.clip == clip) return; _backgroundMusic.clip = clip; _backgroundMusic.Play(); } protected override void OnDestroy() { // 确保在销毁时停止所有声音 _backgroundMusic.Stop(); base.OnDestroy(); } }场景切换时的注意事项:
- 使用
DontDestroyOnLoad保持单例跨场景存在 - 在
Awake中初始化持久化组件 - 确保
OnDestroy中正确释放资源
6. 高级应用:多系统协调销毁
对于复杂的系统,可能需要协调多个单例的销毁顺序:
public class SystemCoordinator : SafeMonoSingleton<SystemCoordinator> { private List<Action> _shutdownActions = new List<Action>(); public void RegisterShutdownAction(Action action) { _shutdownActions.Add(action); } protected override void OnDestroy() { // 按注册顺序逆序执行关闭操作 for (int i = _shutdownActions.Count - 1; i >= 0; i--) { try { _shutdownActions[i]?.Invoke(); } catch (Exception e) { Debug.LogError($"关闭操作执行失败: {e}"); } } base.OnDestroy(); } }这种模式特别适合以下场景:
- 需要确保某些操作在其他系统关闭前执行
- 有相互依赖的系统需要按特定顺序关闭
- 需要集中管理资源释放
7. 性能优化与调试技巧
在处理单例销毁问题时,以下调试技巧可能会很有帮助:
调试工具类:
public static class SingletonDebugger { private static Dictionary<Type, string> _singletonStatus = new Dictionary<Type, string>(); public static void LogSingletonStatus<T>(T instance) where T : MonoBehaviour { var type = typeof(T); if (instance == null) { _singletonStatus[type] = $"[{DateTime.Now}] {type.Name} 实例已销毁"; } else { _singletonStatus[type] = $"[{DateTime.Now}] {type.Name} 实例活跃"; } } public static void PrintAllStatus() { foreach (var status in _singletonStatus.Values) { Debug.Log(status); } } }使用方式:
protected override void OnDestroy() { SingletonDebugger.LogSingletonStatus(this); base.OnDestroy(); SingletonDebugger.PrintAllStatus(); }性能优化建议:
- 避免在
OnDestroy中进行耗时操作 - 对于频繁调用的单例,考虑使用缓存机制
- 使用对象池管理需要频繁创建销毁的对象