41.性能优化-CPU-脚本-资源加载与反序列化
41.1 知识点
资源加载或反序列化为什么会带来开销
资源加载 包括:Resources 加载、AB 包加载、Addressables 加载、UnityWebRequest 加载、WWW 加载、File 加载等。反序列化数据 包括:Json、XML、二进制、ScriptableObject 等。
进行资源加载或反序列化时,主要开销有:
I/O 开销
从磁盘或网络读取会有 I/O 等待,数据需从磁盘/网络进入内存,这一过程会阻塞 CPU,尤其在同步加载时。解析开销
加载成功后要进行数据解析或反序列化,以匹配内部数据结构;部分资源还有解压(如 AB 包的 LZ4、LZMA);反序列化还可能涉及字符串解析、反射等。内存分配与初始化开销
部分资源需要分配 C++ 引擎对象(如 Mesh、Texture、AnimationClip 等)并做初始化,也会带来消耗。
总结: 资源加载与数据反序列化消耗 CPU,本质是把文件中的原始字节流变成可用对象的过程,涉及 I/O、解压、解析、内存分配与类型初始化,这些工作主要由 CPU 在主线程完成,容易造成明显卡顿。过程可简化为:读文件 → 变对象 → 进场景。优化思路就是把这一过程做得更轻、更分散、更可复用。
减小序列化对象
思路: 让需要被读出的对象体积更小、结构更简单,解析更快、计算压力更低。
举例:
- Unity 预设体尽量简单,通过组合方式加载,预设体更精简可提升解析与实例化速度。
- 配置文件尽量用二进制替代文本(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基础框架中缓存池(对象池)部分。
预热
思路: 在玩家不敏感的阶段(如过场、加载界面)提前准备即将用到的资源。
- 预加载资源:缓解 I/O 与反序列化带来的峰值消耗。
- 预实例化资源:缓解内存分配与脚本初始化消耗。
- 预渲染资源:缓解 CPU 到 GPU 的首帧上传与 Shader 编译(纹理、网格、Shader 等首次上传到 GPU)消耗。
举例:
- 预加载资源:在加载界面用同步或异步 API 预加载资源,并可缓存。
- 预实例化资源:用预加载的资源把即将用到的预设体先实例化出来,最好放入对象池。
- 预渲染资源:对预实例化对象做一次预渲染。为避免被主摄像机看到影响体验,常用方式有:
- 隐藏相机 + RenderTexture:用单独摄像机配合渲染纹理渲染需要预渲染的对象,放在该摄像机视野内,在加载协程中多等几帧完成渲染;适合动画、粒子等多帧预渲染。
- Unity API 预渲染:用 CommandBuffer、Graphics.DrawMesh 等做预渲染;适合仅需上传网格、纹理、Shader 变体、材质的情况。
叠加、异步加载场景
思路:
- 方式一(Unity 自带 API):把大关卡拆成多个子场景,按需、分时加载,在合适时机激活或卸载;使用
SceneManager的异步加载场景 API,通过加载模式参数控制是否叠加加载。 - 方式二(自定义场景编辑器):场景为空场景,物体由场景编辑器记录数据,进入场景时全部动态加载;自定义加载策略,在合适距离/时机加载或卸载场景中的物件。
必备知识: Unity 四部曲;Unity 数据持久化;Unity 编辑器开发。
采用合理资源布局
思路: 根据项目情况选择 Resources、AB 包、Addressables 等,并做好资源布局。
常见方案对比:
- Resources:适合小体量或原型;会打进包、无法热更、依赖不可控、启动时做索引扫描,大项目尽量少用或不用。
- AssetBundle(自管):需自己实现下载、缓存、版本策略,工程复杂度高,可控性强。
- Addressables:官方维护,热更与缓存策略完整、使用方便,可控性不如自管 AB。
资源布局建议:
- 使用 AB 包或 Addressables 时合理分包:减少跨包依赖,包内高内聚,单包大小适中(移动端常见 10~50MB),并发下载不宜过多(如 3~5 个)。
- Shader 剔除无用关键字,减少变体;尽量复用材质,降低实例化消耗。
- 配置表 尽量集中,预设体少做可配置属性,多用 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