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

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


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

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


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


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


文章目录

    • 【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题
      • 一、问题起点:LoadSceneAsync 导致的卡顿
      • 二、卡顿原因分析
      • 三、常规优化尝试
        • 1. allowSceneActivation = false
        • 2. 延迟帧 / 加载动画
      • 四、核心解决方案:预热 + 资源卸载
        • 1. 什么是场景预热(Prewarm)?
        • 2. 场景资源未释放问题
      • 五、完善场景管理系统:SceneFlowManager
        • 1. 支持配置化管理 EqSceneConfig
        • 2. 支持 Key 方式加载
        • 3. 支持场景预热接口
      • 六、新增释放资源接口
      • 七、完整流程总结
      • 八、性能实测对比
      • 九、扩展:自动预热与内存调度
      • 十、结语:让 Unity 多场景系统真正高效
        • 1. 总结
        • 2. 源码

一、问题起点: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 几乎无卡顿 加载+释放资源 40ms 40ms 150MB↓ 无卡顿

直接加载,出现卡顿(掉帧)
【Unity优化】Unity多场景加载优化与资源释放完整指南:解决Additive加载卡顿、预热、卸载与内存释放问题_unity 场景加载优化

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


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

你可以设置:

  • 定时自动预热(玩家未操作时)
  • 内存压力大时调用 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 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(); 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 scenesToUnload = new List(); 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} 预热完成并卸载\"); } ///  /// 释放指定场景对应的未被引用资源,确保卸载后内存回收 ///  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}] 资源释放完成\"); } }}