关注

【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题

【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题

本文将完整梳理 Unity 中通过 SceneManager.LoadSceneAsync 使用 Additive 模式加载子场景时出现的卡顿问题,分析其本质,提出不同阶段的优化策略,并最终实现一个从预热、加载到资源释放的高性能、低内存场景管理系统。本文适用于(不使用Addressables 的情况下)需要频繁加载子场景的 VR/AR/大地图/分区模块化项目。


前文主要是一些发现问题,解决问题的文档记录。
查看源码,请跳转至文末!


在这里插入图片描述



一、问题起点:LoadSceneAsync 导致的卡顿

在项目开发过程中,当我们使用如下代码进行 Additive 场景加载时:

AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("YourScene", LoadSceneMode.Additive);

你会发现:

  • 第一次加载某个场景时卡顿极为明显
  • 后续加载相同场景不卡顿,表现正常
  • 即使使用 allowSceneActivation = false 先加载至 0.9,再激活,也无法解决卡顿。

二、卡顿原因分析

Unity 场景加载包括两个阶段:

  1. 资源加载阶段(读取场景所需的纹理、Mesh、Prefab 等)
  2. 激活阶段(触发 Awake/Start、构建场景结构)

而第一次加载时会触发:

  • Shader Compile
  • 静态 Batching
  • Occlusion Culling 计算
  • 实例化所有场景对象

这些过程即使异步,也依然可能在 allowSceneActivation=true 时集中执行,导致帧冻结。


三、常规优化尝试

1. allowSceneActivation = false
asyncLoad.allowSceneActivation = false;
while (asyncLoad.progress < 0.9f) yield return null;
yield return new WaitForSeconds(0.5f);
asyncLoad.allowSceneActivation = true;

结果:激活时依旧卡顿。

2. 延迟帧 / 加载动画

只能缓解体验,不能真正解决第一次激活的卡顿


四、核心解决方案:预热 + 资源卸载

1. 什么是场景预热(Prewarm)?

在用户进入目标场景之前,提前加载该场景、触发资源加载、初始化内存,再卸载掉。

这样用户真正进入场景时:

  • 所有资源都在缓存中(Unity 会延后释放)
  • 场景结构早已解析,第二次加载快很多
IEnumerator PrewarmSceneCoroutine(string sceneName)
{
    var loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
    loadOp.allowSceneActivation = true;
    while (!loadOp.isDone) yield return null;

    yield return null;
    yield return null; // 等待几帧确保初始化

    var unloadOp = SceneManager.UnloadSceneAsync(sceneName);
    while (!unloadOp.isDone) yield return null;
}
2. 场景资源未释放问题

你会发现:预热+卸载后并不会立即释放资源!

Unity 会保留一部分资源在内存中,直到调用:

Resources.UnloadUnusedAssets();

所以你必须加入如下逻辑:

yield return Resources.UnloadUnusedAssets();

五、完善场景管理系统:SceneFlowManager

在项目中,我们将所有的加载逻辑封装在 SceneFlowManager 中。

1. 支持配置化管理 EqSceneConfig
[System.Serializable]
public class EqSceneEntry
{
    public string key;
    public string sceneName;
}

[CreateAssetMenu]
public class EqSceneConfig : ScriptableObject
{
    public List<EqSceneEntry> scenes;
}
2. 支持 Key 方式加载
public void LoadSceneAdditiveByKey(string key) => LoadSceneAdditive(GetSceneNameByKey(key));
3. 支持场景预热接口
public void PrewarmScene(string sceneName)
{
    if (IsSceneLoaded(sceneName)) return;
    StartCoroutine(PrewarmSceneCoroutine(sceneName));
}

六、新增释放资源接口

为了真正释放场景相关的资源,新增 ReleaseSceneResources 方法:

public void ReleaseSceneResources(string sceneName)
{
    if (IsSceneLoaded(sceneName))
    {
        StartCoroutine(UnloadAndReleaseCoroutine(sceneName));
    }
    else
    {
        StartCoroutine(ReleaseOnlyCoroutine());
    }
}

private IEnumerator UnloadAndReleaseCoroutine(string sceneName)
{
    yield return SceneManager.UnloadSceneAsync(sceneName);
    yield return Resources.UnloadUnusedAssets();
}

private IEnumerator ReleaseOnlyCoroutine()
{
    yield return Resources.UnloadUnusedAssets();
}

七、完整流程总结

  1. 项目启动时

    • 初始化 SceneFlowManager
    • 预热即将访问的场景(不会激活)
  2. 进入新场景

    • 调用 LoadSceneAdditiveByKey(key) 平滑加载场景
  3. 离开场景

    • 调用 ReleaseSceneResourcesByKey(key) 卸载并释放内存
  4. 避免过早 Resources.UnloadUnusedAssets()

    • 建议只在真正切场景后调用,避免误删仍在用资源

八、性能实测对比

流程首次加载帧耗时第二次加载帧耗时内存占用卡顿感受
直接加载80ms+40ms+300MB↑明显卡顿
预热+加载30ms↓20ms↓200MB几乎无卡顿
加载+释放资源40ms40ms150MB↓无卡顿

直接加载,出现卡顿(掉帧)
在这里插入图片描述

预热+加载,无掉帧
在这里插入图片描述


九、扩展:自动预热与内存调度

你可以设置:

  • 定时自动预热(玩家未操作时)
  • 内存压力大时调用 ReleaseSceneResources
  • 按访问频率记录预热优先级

十、结语:让 Unity 多场景系统真正高效

1. 总结

本方案从 SceneManager.LoadSceneAsync 的卡顿问题出发,经历:

  • allowSceneActivation 控制加载
  • 手动预热场景
  • 引入资源释放

最终构建了一个完整的 SceneFlowManager

2. 源码

完整代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Eqgis.Runtime.Scene
{
    public class SceneFlowManager : MonoBehaviour
    {
        public static SceneFlowManager Instance { get; private set; }

        [Tooltip("常驻场景名称,不参与卸载")]
        private string persistentSceneName;

        [Tooltip("场景配置文件")]
        public EqSceneConfig sceneConfig;

        private Dictionary<string, string> keyToSceneMap;

        public void Awake()
        {// 自动记录当前激活场景为 PersistentScene
            persistentSceneName = SceneManager.GetActiveScene().name;
            Android.EqLog.d("SceneFlowManager", $"[SceneFlowManager] PersistentScene 自动设置为:{persistentSceneName}");

            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }

            Instance = this;
            DontDestroyOnLoad(gameObject);

            InitSceneMap();
        }

        private void InitSceneMap()
        {
            keyToSceneMap = new Dictionary<string, string>();
            if (sceneConfig != null)
            {
                foreach (var entry in sceneConfig.scenes)
                {
                    if (!keyToSceneMap.ContainsKey(entry.key))
                    {
                        keyToSceneMap.Add(entry.key, entry.sceneName);
                    }
                    else
                    {
                        Debug.LogWarning($"重复的场景 Key:{entry.key}");
                    }
                }
            }
            else
            {
                Debug.LogWarning("未指定 EqSceneConfig,SceneFlowManager 无法使用 key 加载场景");
            }
        }

        // 根据 key 获取真实场景名
        private string GetSceneNameByKey(string key)
        {
            if (keyToSceneMap != null && keyToSceneMap.TryGetValue(key, out var sceneName))
                return sceneName;

            Debug.LogError($"未找到 key 对应的场景名: {key}");
            return null;
        }

        // 通过 Key 加载 Additive 场景
        public void LoadSceneAdditiveByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                LoadSceneAdditive(sceneName);
            }
        }
        // 通过 Key 加载 Single 场景
        public void LoadSceneSingleByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                LoadSceneSingle(sceneName);
            }
        }

        // 通过 Key 卸载场景
        public void UnloadSceneByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                UnloadScene(sceneName);
            }
        }

        // 加载场景名(Additive)
        public void LoadSceneAdditive(string sceneName)
        {
            if (!IsSceneLoaded(sceneName))
            {
                //SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
                StartCoroutine(LoadSceneAdditiveCoroutine(sceneName));
            }
        }

        // 加载场景名(Additive)
        private IEnumerator LoadSceneAdditiveCoroutine(string sceneName)
        {
            AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
            //asyncLoad.allowSceneActivation = false;

            //while (asyncLoad.progress < 0.9f)
            //{
            //    yield return null; // 等待加载完成(进度最多到0.9)
            //}

            //// 此时可以延迟几帧或做加载动画等处理
            //yield return new WaitForSeconds(0.5f);
            //asyncLoad.allowSceneActivation = true; // 手动激活场景

            // 参考:https://docs.unity3d.com/2021.3/Documentation/ScriptReference/SceneManagement.SceneManager.LoadSceneAsync.html
            while (!asyncLoad.isDone)
            {
                yield return null;
            }
        }

        // 加载场景名(Single)
        public void LoadSceneSingle(string sceneName)
        {
            if (!IsSceneLoaded(sceneName))
            {
                SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
            }
        }

        // 卸载指定场景
        public void UnloadScene(string sceneName)
        {
            if (sceneName == persistentSceneName) return;

            if (IsSceneLoaded(sceneName))
            {
                SceneManager.UnloadSceneAsync(sceneName);
            }
        }

        // 卸载所有非常驻场景
        public void UnloadAllNonPersistentScenes()
        {
            StartCoroutine(UnloadAllExceptPersistent());
        }

        private IEnumerator UnloadAllExceptPersistent()
        {
            List<string> scenesToUnload = new List<string>();

            for (int i = 0; i < SceneManager.sceneCount; i++)
            {
                var scene = SceneManager.GetSceneAt(i);
                if (scene.name != persistentSceneName)
                {
                    scenesToUnload.Add(scene.name);
                }
            }

            foreach (string sceneName in scenesToUnload)
            {
                AsyncOperation op = SceneManager.UnloadSceneAsync(sceneName);
                while (!op.isDone)
                {
                    yield return null;
                }
            }
        }

        public bool IsSceneLoaded(string sceneName)
        {
            for (int i = 0; i < SceneManager.sceneCount; i++)
            {
                if (SceneManager.GetSceneAt(i).name == sceneName)
                    return true;
            }
            return false;
        }

        public void SetActiveScene(string sceneName)
        {
            if (IsSceneLoaded(sceneName))
            {
                SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneName));
            }
        }

        public void SetActiveSceneByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                SetActiveScene(sceneName);
            }
        }

        // 通过 Key 预热一个场景(Additive 预加载后立即卸载)
        public void PrewarmSceneByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                PrewarmScene(sceneName);
            }
        }

        // 通过场景名预热一个场景
        public void PrewarmScene(string sceneName)
        {
            // 若已加载,无需预热
            if (IsSceneLoaded(sceneName))
            {
                Debug.Log($"[SceneFlowManager] 场景 {sceneName} 已加载,跳过预热");
                return;
            }

            StartCoroutine(PrewarmSceneCoroutine(sceneName));
        }

        private IEnumerator PrewarmSceneCoroutine(string sceneName)
        {
            Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 开始预热场景:{sceneName}");

            AsyncOperation loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
            loadOp.allowSceneActivation = true;

            while (!loadOp.isDone)
                yield return null;

            // 延迟几帧以确保资源初始化完成
            yield return null;
            yield return null;

            Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 场景 {sceneName} 加载完毕,开始卸载");

            AsyncOperation unloadOp = SceneManager.UnloadSceneAsync(sceneName);
            while (!unloadOp.isDone)
                yield return null;

            Android.EqLog.d("SceneFlowManager", "[SceneFlowManager] 场景 {sceneName} 预热完成并卸载");
        }

        /// <summary>
        /// 释放指定场景对应的未被引用资源,确保卸载后内存回收
        /// </summary>
        public void ReleaseSceneResourcesByKey(string key)
        {
            string sceneName = GetSceneNameByKey(key);
            if (!string.IsNullOrEmpty(sceneName))
            {
                ReleaseSceneResources(sceneName);
            }
        }

        public void ReleaseSceneResources(string sceneName)
        {
            if (sceneName == persistentSceneName)
            {
                Debug.LogWarning($"不能释放常驻场景[{sceneName}]的资源");
                return;
            }

            if (IsSceneLoaded(sceneName))
            {
                // 场景已加载,先卸载后释放资源
                AsyncOperation unloadOp = SceneManager.UnloadSceneAsync(sceneName);
                StartCoroutine(ReleaseResourcesAfterUnload(unloadOp, sceneName));
            }
            else
            {
                // 场景已卸载,直接释放资源
                StartCoroutine(ReleaseResourcesDirect(sceneName));
            }
        }

        private IEnumerator ReleaseResourcesAfterUnload(AsyncOperation unloadOp, string sceneName)
        {
            yield return unloadOp;

            Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 已卸载,开始释放未使用资源");

            AsyncOperation unloadUnused = Resources.UnloadUnusedAssets();

            yield return unloadUnused;

            Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 资源释放完成");
        }

        private IEnumerator ReleaseResourcesDirect(string sceneName)
        {
            Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 已卸载,直接释放未使用资源");

            AsyncOperation unloadUnused = Resources.UnloadUnusedAssets();

            yield return unloadUnused;

            Android.EqLog.d("SceneFlowManager", $"场景 [{sceneName}] 资源释放完成");
        }

    }
}

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_41140324/article/details/149513285

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--