41.资源加载与反序列化

41.性能优化-CPU-脚本-资源加载与反序列化


41.1 知识点

资源加载或反序列化为什么会带来开销

资源加载 包括:Resources 加载、AB 包加载、Addressables 加载、UnityWebRequest 加载、WWW 加载、File 加载等。反序列化数据 包括:Json、XML、二进制、ScriptableObject 等。

进行资源加载或反序列化时,主要开销有:

  1. I/O 开销
    从磁盘或网络读取会有 I/O 等待,数据需从磁盘/网络进入内存,这一过程会阻塞 CPU,尤其在同步加载时。

  2. 解析开销
    加载成功后要进行数据解析或反序列化,以匹配内部数据结构;部分资源还有解压(如 AB 包的 LZ4、LZMA);反序列化还可能涉及字符串解析、反射等。

  3. 内存分配与初始化开销
    部分资源需要分配 C++ 引擎对象(如 Mesh、Texture、AnimationClip 等)并做初始化,也会带来消耗。

总结: 资源加载与数据反序列化消耗 CPU,本质是把文件中的原始字节流变成可用对象的过程,涉及 I/O、解压、解析、内存分配与类型初始化,这些工作主要由 CPU 在主线程完成,容易造成明显卡顿。过程可简化为:读文件 → 变对象 → 进场景。优化思路就是把这一过程做得更轻、更分散、更可复用

减小序列化对象

思路: 让需要被读出的对象体积更小、结构更简单,解析更快、计算压力更低。

举例:

  1. Unity 预设体尽量简单,通过组合方式加载,预设体更精简可提升解析与实例化速度。
  2. 配置文件尽量用二进制替代文本(Json、XML 等),体积更小、结构更简单,有利于处理效率。

必备知识: Unity 四部曲之 Unity 入门;Unity 数据持久化四部曲——2 进制。

公共数据配置化

思路: 把游戏中常用的公共数据做成配置文件,全局只保留一份,一般在游戏初期加载并解析;各模块或对象使用时直接引用,避免重复加载、减少数据冗余。

举例: 物品配置表存所有物品信息(ID、图标、模型、名称、价格、品质、各属性等);游戏中表示物品时只持有一个 ID,通过 ID 在表中查对应数据使用。

必备知识: Unity 数据持久化四部曲;Unity 进阶之 ScriptableObject。

异步加载 + 分帧实例化

思路: 不在主线程同步阻塞等待,而是用异步 API 加载资源(避免主线程卡顿),加载完成需要实例化时分帧处理(避免单帧集中实例化造成卡顿)。

举例: 使用 Resources、AB 包、Addressables、UnityWebRequest 等提供的异步加载接口;资源加载完成后,若要实例化多个对象,分多帧执行。

示例:协程内异步加载预设体,再分帧实例化多个。

using UnityEngine;

// 示例思路:异步加载后分帧实例化
IEnumerator LoadAndInstantiateInFrames(string prefabPath, int instanceCount)
{
    // 异步加载(具体 API 依项目所用方案:Resources.LoadAsync / Addressables 等)
    ResourceRequest request = Resources.LoadAsync<GameObject>(prefabPath);
    yield return request;
    GameObject prefab = request.asset as GameObject;
    if (prefab == null) yield break;

    // 分帧实例化,每帧实例化少量,避免单帧卡顿
    const int perFrame = 5;
    int instantiated = 0;
    while (instantiated < instanceCount)
    {
        for (int i = 0; i < perFrame && instantiated < instanceCount; i++)
        {
            Object.Instantiate(prefab);
            instantiated++;
        }
        yield return null;
    }
}

必备知识: Unity 四部曲之 Unity 基础。

缓存

思路: 把加载成本高的结果暂存到内存,下次直接复用,用内存换性能。

举例: 资源用合适容器(如 Dictionary)缓存在内存,需要时直接取用;已实例化的场景对象用对象池复用。

注意: 缓存需在合适时机清理,避免内存泄漏。

示例:用字典缓存已加载资源,避免重复加载。

using System.Collections.Generic;
using UnityEngine;

// 示例:缓存已加载资源,按 key 复用
private Dictionary<string, GameObject> prefabCache = new Dictionary<string, GameObject>();

GameObject GetOrLoadPrefab(string prefabPath)
{
    if (prefabCache.TryGetValue(prefabPath, out GameObject cached))
        return cached;
    GameObject loaded = Resources.Load<GameObject>(prefabPath);
    if (loaded != null)
        prefabCache[prefabPath] = loaded;
    return loaded;
}

必备知识: Unity 四部曲;Unity 热更新方案 Lua —— AB 包部分;Unity 进阶之 Addressables;Unity基础框架中缓存池(对象池)部分。

预热

思路: 在玩家不敏感的阶段(如过场、加载界面)提前准备即将用到的资源。

  1. 预加载资源:缓解 I/O 与反序列化带来的峰值消耗。
  2. 预实例化资源:缓解内存分配与脚本初始化消耗。
  3. 预渲染资源:缓解 CPU 到 GPU 的首帧上传与 Shader 编译(纹理、网格、Shader 等首次上传到 GPU)消耗。

举例:

  1. 预加载资源:在加载界面用同步或异步 API 预加载资源,并可缓存。
  2. 预实例化资源:用预加载的资源把即将用到的预设体先实例化出来,最好放入对象池。
  3. 预渲染资源:对预实例化对象做一次预渲染。为避免被主摄像机看到影响体验,常用方式有:
    • 隐藏相机 + RenderTexture:用单独摄像机配合渲染纹理渲染需要预渲染的对象,放在该摄像机视野内,在加载协程中多等几帧完成渲染;适合动画、粒子等多帧预渲染。
    • Unity API 预渲染:用 CommandBuffer、Graphics.DrawMesh 等做预渲染;适合仅需上传网格、纹理、Shader 变体、材质的情况。

叠加、异步加载场景

思路:

  • 方式一(Unity 自带 API):把大关卡拆成多个子场景,按需、分时加载,在合适时机激活或卸载;使用 SceneManager 的异步加载场景 API,通过加载模式参数控制是否叠加加载。
  • 方式二(自定义场景编辑器):场景为空场景,物体由场景编辑器记录数据,进入场景时全部动态加载;自定义加载策略,在合适距离/时机加载或卸载场景中的物件。

必备知识: Unity 四部曲;Unity 数据持久化;Unity 编辑器开发。

采用合理资源布局

思路: 根据项目情况选择 Resources、AB 包、Addressables 等,并做好资源布局。

常见方案对比:

  • Resources:适合小体量或原型;会打进包、无法热更、依赖不可控、启动时做索引扫描,大项目尽量少用或不用。
  • AssetBundle(自管):需自己实现下载、缓存、版本策略,工程复杂度高,可控性强。
  • Addressables:官方维护,热更与缓存策略完整、使用方便,可控性不如自管 AB。

资源布局建议:

  1. 使用 AB 包或 Addressables 时合理分包:减少跨包依赖,包内高内聚,单包大小适中(移动端常见 10~50MB),并发下载不宜过多(如 3~5 个)。
  2. Shader 剔除无用关键字,减少变体;尽量复用材质,降低实例化消耗。
  3. 配置表 尽量集中,预设体少做可配置属性,多用 ID 在配置表中查数据复用。

41.2 知识点代码

Lesson41_性能优化_CPU_脚本_资源加载与反序列化.cs

public class Lesson41_性能优化_CPU_脚本_资源加载与反序列化
{
    #region 知识点一 资源加载或反序列化为什么会带来开销

    //资源加载
    //Resources加载、AB包加载、Addressables加载、UnityWebRequest加载、WWW加载、File加载 等等
    //反序列化数据
    //Json、XML、二进制、ScriptableObject 等等

    //在进行资源加载或反序列化时带来的主要开销有:
    //1.IO开销
    //磁盘或网络读取会有I/O等待,等待数据从磁盘或网络加载到内存
    //这个过程会阻塞CPU(特别是同步加载时)

    //2.解析开销
    //加载成功后又需要进行数据解析或反序列化,用于匹配内部类的结构
    //对于部分资源可能还会存在解压缩过程(比如AB包的LZ4和LZMA格式)
    //反序列化时可能还会用到字符串解析、反射等等手段进行处理

    //3.内存分配初始化开销
    //对于一些资源对象,我们需要分配C++引擎对象(比如Mesh、Texture、AnimationClip等等)
    //并且还需要对对应对象进行初始化,这个过程也会带来消耗

    //总的来说
    //资源加载和数据反序列化会吃 CPU
    //是因为它们本质上是把文件里的原始字节流转化为可用对象的过程
    //涉及 I/O、解压、解析、内存分配和类型初始化
    //这些工作目前只能由 CPU 完成,而且往往集中在主线程执行,所以会造成明显的性能开销
    //这个过程可以简单整理为 读文件—>变对象—>进场景
    //因此我们主要的优化思路就是把这个过程做得更轻、更分散、更可复用

    #endregion

    #region 知识点二 减小序列化对象

    //主要思路:
    //让需要被读出来的对象体积更小、结构更简单
    //这样解析就会更快,计算压力就会更低

    //举例:
    //1.Unity中的预设体尽量做得简单,通过组合的形式加载
    //  让预设体更精简,可以提升解析、实例化速度
    //2.配置文件尽量用2进制代替文本配置(Json、XML等)
    //  让配置文件体积更小,结构更简单,可以提升处理效率

    //必备知识:
    //Unity四部曲之Unity入门
    //Unity数据持久化四部曲——2进制

    #endregion

    #region 知识点三 公共数据配置化

    //主要思路:
    //把游戏中会常用的公共数据做成配置文件的形式,游戏中保证只存在一份,并一般在游戏初期加载解析他们
    //当有模块或对象要使用这些数据时直接使用它们即可
    //可以避免重复加载,并且可以减少数据冗余

    //举例:
    //物品配置中包含所有物品的各种信息(ID、图标、模型、名称、价格、品质、各属性 等等)
    //游戏中表示物品信息时只需要一个ID,通过ID在该表中找到对应的各种数据来使用

    //必备知识:
    //Unity数据持久化四部曲
    //Unity进阶之ScriptableObject

    #endregion

    #region 知识点四 异步加载 + 分帧实例化

    //主要思路:
    //不要在主线程同步卡着等待
    //而是通过异步API加载资源(避免主线程阻塞卡顿),并且加载成功要实例化时分帧处理(避免主线程阻塞卡顿)

    //举例:
    //利用Resources、AB包、Addressables、UnityWebRequest等加载方式中的异步方法加载资源
    //资源加载成功后,当需要实例化n个对象时分多帧去进行处理

    //必备知识:
    //Unity四部曲之Unity基础

    #endregion

    #region 知识点五 缓存

    //主要思路:
    //把付出昂贵代价加载出来的结果暂存在内存中,下次直接复用
    //相当于用内存换性能

    //举例:
    //对于资源对象,我们可以用合适的容器将其缓存在内存中
    //下次如果还要使用,直接获取使用即可
    //对于已经实例化的场景中的对象,我们可以利用缓存池不停复用它们

    //注意:
    //缓存内容要在合适的时候进行清理,避免内存泄漏

    //必备知识:
    //Unity四部曲
    //Unity热更新方案Lua —— AB包部分
    //Unity进阶之Addressables
    //Unity程序基础小框架中缓存池(对象池)部分,可选修

    #endregion

    #region 知识点六 预热

    //主要思路:
    //在玩家不敏感的时机(过场景的加载界面等)
    //提前准备将要使用的资源
    //1.预加载资源:解决IO和反序列化消耗
    //2.预实例化资源:解决内存分配和初始化脚本的消耗
    //3.预渲染资源:解决CPU到GPU的首帧上传和Shader编译(把内存中纹理、网格、Shader等上传到GPU)消耗

    //举例:
    //1.预加载资源:
    //  在加载界面时,利用同步或异步API预加载资源(可缓存下来)
    //2.预实例化资源:
    //  利用预加载的资源将即将使用的预设体预实例化出来,并且最好放入缓存池(对象池)中
    //3.预渲染资源:
    //  利用预实例化的资源进行预先渲染
    //  为了避免预渲染时被主摄像机看到,影响玩家体验
    //  一般会采用
    //  3-1.隐藏相机+RenderTexture
    //      用一个摄像机使用渲染纹理进行渲染,将想要预渲染的对象放在该摄像机可视范围内
    //      在加载协程中多等待几帧,用于渲染对象
    //      比较适用于动画、粒子等资源预渲染,因为可以进行多帧渲染
    //  3-2.利用UnityAPI进行预渲染
    //      利用 CommandBuffer 和 Graphics.DrawMesh 进行预渲染
    //      比较适用于只想上传网格、纹理、Shader变体、材质的情况

    #endregion

    #region 知识点七 叠加、异步加载场景

    //主要思路:
    //方式一:利用Unity自带API
    //  把大关卡拆成多个子场景,加载场景时按需加载、分时加载、并且在合适的时机激活或卸载场景
    //  可以利用Unity中SceneManager中的异步加载场景API,并通过加载模式参数控制是否叠加加载
    //方式二:自定义场景编辑器
    //  场景为空场景,场景中物件都是通过场景编辑器记录数据,进入场景时所有对象都是动态加载的
    //  自定义加载策略,在合适的距离时机加载对应场景中物件对象,在合适的时机卸载场景中物件对象

    //必备知识:
    //Unity四部曲
    //Unity数据持久化
    //Unity编辑器开发

    #endregion

    #region 知识点八 采用合理资源布局

    //主要思路:
    //在选择Resources、AB包、Addressables等资源加载方式时
    //应采用适合自己项目情况的方案进行资源布局
    //比如:
    //  Resources:
    //  只适合小体量或原型项目开发
    //  它会打进包、无法热更、依赖不可控、启动时会做索引扫描,建议大项目尽量少用甚至不用
    //  AssetBundle(自己管理、可控性更强):
    //  需要完全自定义下载、缓存、版本策略;工程复杂度较高,但可控性强
    //  Addressables:
    //  官方维护、热更与缓存策略完整、使用方便
    //  可控性不如AssetBundle

    //  资源布局方面
    //  1.使用AB包或Addressables时应合理分包
    //  减少跨包依赖,包内资源应高度内聚,并且单包大小要适中(移动端常见10~50MB),并发下载不要超过3~5个
    //  2.Shader应该剔除无用关键字,避免过多变体的产生
    //  尽量复用材质,可以减少实例化时消耗
    //  3.配置表尽量集中化,预设体中不要太多可配置属性,尽量都通过ID在配置表中获取数据来复用
    //  等等

    #endregion
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏