115.Unity性能优化基础知识总结

  1. 115.总结
    1. 115.1 知识点
      1. 主要内容
      2. 主要学习内容
      3. 性能优化学习意义
      4. 万人同屏实践预告
      5. 总结
    2. 115.2 核心要点速览
      1. 性能分析工具概述
      2. Profiler 窗口基础
        1. 打开方式与模式
        2. 窗口结构
        3. 顶部工具栏关键功能
        4. CPU Usage 模块
        5. GPU Usage 模块
      3. Profiler 窗口进阶模块
        1. Rendering 模块
        2. Memory 模块
        3. Audio 模块
        4. Video 模块
        5. Physics 模块
        6. Physics 2D 模块
        7. UI 和 UI Details 模块
        8. Realtime GI 模块
        9. Virtual Texturing 模块
        10. File Access 模块
        11. Asset Loading 模块
        12. Profiler 脚本控制 API
      4. Memory Profiler 窗口
        1. Summary 页签
        2. Unity Objects 页签
        3. All Of Memory 页签
      5. Unity 机制问题
        1. 脚本挂载检测
        2. 生命周期函数注意事项
        3. Debug API 性能消耗
        4. 垂直同步(VSync)
        5. 排除其他应用程序影响
      6. CPU 脚本优化
        1. 计时判断代码执行效率
        2. 计时基本原理
        3. Unity Time 类计时
        4. C# Stopwatch 类计时
        5. 计时类封装(IDisposable + using)
        6. 组件获取与 Update
        7. Update 函数优化
        8. 协程与延迟函数
        9. 本机托管桥接
        10. GameObject 与 Transform
        11. 数据结构与缓存
        12. 禁用未使用对象
        13. 资源加载与预渲染
      7. CPU 图形优化
        1. DrawCall 与批处理
        2. 动态批处理限制
        3. 静态批处理限制
        4. GPU Instancing 限制
        5. SRP Batcher 限制
        6. 手动合并网格
      8. CPU 物理优化
        1. Physics Debugger
        2. 时间步长设置
        3. 碰撞层矩阵
        4. 碰撞器性能
        5. 刚体优化
        6. 碰撞检测优化
      9. GPU 优化
        1. 影响因素
        2. 增强渲染性能
      10. UI 系统优化
        1. Canvas 组件优化
        2. RaycastTarget 优化
        3. Canvas 禁用与 Animator 避免
        4. 世界空间 Canvas 的相机定义
        5. 隐藏 UI 元素的优化
        6. ScrollRect 优化
        7. Text 拦截全屏 UI 交互
        8. 其他 UI 优化方案
      11. 粒子系统优化
        1. CPU 侧优化
        2. GPU 侧优化
      12. 着色器优化
      13. 内存优化
        1. 移动端渲染性能优化
        2. 资源优化:音频
        3. 资源优化:视频
        4. 资源优化:纹理
        5. 资源优化:网格和动画
        6. 资源优化:材质和 Shader
        7. 资源优化:AB 包和 Resources
        8. 内存管理:基础优化技巧
        9. 内存管理:运行时高频操作优化
        10. 内存管理:资源管理与 GC 进阶
    3. 115.3 面试题精选
      1. 基础题
        1. 1. Profiler 内嵌模式与独立进程模式有何区别?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. CPU Usage 里 Total 与 Self 各表示什么?如何用来找瓶颈?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 脚本里哪些「看起来无害」的写法会带来持续性能损耗?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 开启 GPU Usage 为何会扭曲性能结论?URP/HDRP 下要注意什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 如何从 Rendering 与合批角度判断 DrawCall 是否是瓶颈?批处理体系怎么选?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. Profiler Memory 与 Memory Profiler 包如何分工?驻留内存与已分配内存有何不同?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 如何用 Profiler API 做自定义区间采样与运行时内存查询?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 用 Memory Profiler 做「泄漏」排查时,推荐的操作顺序是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 什么是本机-托管桥接?脚本侧如何系统性减少这类开销?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. ComputeShader 的执行模型是什么?numthreads 与 Dispatch 如何决定总线程数?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. 托管堆上哪些习惯会放大 GC 压力?非托管与 Native 资源要如何收尾?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

115.总结


115.1 知识点

主要内容

主要学习内容

性能优化学习意义

万人同屏实践预告

总结


115.2 核心要点速览

性能分析工具概述

性能分析工具用于检测和分析 Unity 应用程序性能,帮助开发者定位性能瓶颈。

核心工具

工具 用途
Profiler 窗口 实时监控 CPU、GPU、内存、渲染等模块性能
Memory Profiler 详细分析内存快照,定位内存泄漏和资源占用
Physics Debugger 可视化调试物理系统,检查碰撞体状态

Profiler 主要模块:CPU Usage、GPU Usage、Memory、Rendering、Audio、Video、Physics、Physics 2D、UI 等。

使用注意事项

  • 最终性能测试不要用 Development Build(有额外开销)
  • 分析时关闭其他程序,避免干扰
  • 多次测试取平均值,排除偶然因素

Profiler 窗口基础

打开方式与模式

模式 打开方式 特点
内嵌模式 Window → Analysis → Profiler 集成在编辑器内,调试方便,但有采样干扰
独立进程模式 Window → Analysis → Profiler (Standalone Process) 独立进程运行,干扰更小,适合大型项目、真机分析

窗口结构

  • A 模块列表:切换要观察的数据模块(CPU Usage、GPU Usage、Memory 等)
  • B 顶部控制区:选择目标、控制录制/播放、切换帧
  • C 帧图表区:展示各模块随时间变化的采样曲线
  • D 详细信息面板:展示当前模块在选中帧上的细项数据

顶部工具栏关键功能

功能 作用 注意事项
模块选择 勾选要采集的模块 不需要的模块关闭,减少开销
模式下拉框 选择分析目标(Play/Edit/远程连接) 真机分析需开启 Development Build
录制开关 开始/停止采样 关键操作前再开,时间轴更干净
深度分析 对 C# 方法细粒度采样 开销大,仅定位问题时短时间开启
调用栈 记录分配相关的调用栈信息 增加开销,需要时再开

偏好设置建议

  • Frame Count:建议改高(如 600),便于查看更长历史数据
  • Default recording state:建议 Enabled,自动录制

CPU Usage 模块

作用:展示每一帧 CPU 花费的时间,按类别细分,是定位 CPU 瓶颈的核心工具。

常见 CPU 开销来源:脚本逻辑、物理系统、动画计算、渲染准备、资源加载、GC、UI 系统等。

分析窗口颜色含义

颜色块 含义
Rendering 渲染准备时间(Camera.Render、生成 DrawCall)
Scripts C# 脚本执行时间(Update、协程等)
Physics 物理计算时间
Animation 动画相关时间
GarbageCollector GC 回收时间
VSync 垂直同步等待时间
UI Canvas 更新、重建、批处理等

参考线16ms(60FPS)33ms(30FPS)66ms(15FPS),快速判断是否超标。

四种视图

视图 特点 适用场景
TimeLine 横向时间轴,按线程显示 查看函数调用位置、线程并发/串行
Hierarchy 调用栈层级展示 快速发现耗时最多的调用分支
Inverted Hierarchy 倒置调用树 分析某函数被谁频繁调用
Raw Hierarchy 原始采样数据 查看完整调用链

关键列

  • Total:函数含子函数占总时间百分比
  • Self:函数自身耗时百分比
  • Calls:调用次数
  • GC Alloc:GC 分配量(Byte)
  • Time ms / Self ms:总耗时 / 自身耗时

两个根节点EditorLoop(编辑器主循环)、PlayerLoop(游戏运行时主循环)。

GPU Usage 模块

作用:分析 GPU 各渲染阶段耗时,定位渲染瓶颈。

常见 GPU 开销来源:几何处理、光照计算、渲染输出、屏幕后处理、纹理处理、特效处理等。

重要限制

  • 采集会禁用 Graphics Jobs,影响性能测试真实性
  • URP/HDRP 不支持详细 GPU 时间采样
  • 不支持所有平台(iOS 可用 Xcode GPU Frame Debugger)

分析窗口颜色含义

颜色块 含义
Opaque 渲染不透明对象时间
Transparent 渲染透明对象时间
Shadows/Depth 阴影贴图/深度贴图时间
PostProcess 屏幕后期处理耗时
Other 其它 GPU 开销(含 SRP)

Profiler 窗口进阶模块

Rendering 模块

作用:显示一帧渲染时的数据量(批次数、顶点数等),帮助判断 DrawCall、材质切换、几何量等渲染因素是否导致性能问题。

核心指标

指标 含义 优化方向
Batches Count 批处理数量(Draw Call 次数) 合批优化(静态/动态批处理、GPU Instancing)
SetPass Calls Shader Pass 切换次数 减少材质和 Shader 变体数量
Triangles / Vertices 渲染的三角形/顶点数量 控制模型复杂度,移动端尤其注意
Dynamic/Static Batching 动态/静态批处理统计 评估合批是否生效
Used Textures 使用的纹理数量与内存 检查贴图分辨率、压缩格式

使用场景:怀疑 DrawCall 过高、中低端机发热掉电快、大量实时光照/粒子后掉帧。

Memory 模块

作用:监视内存使用情况,识别内存占用来源、内存泄漏、GC 频繁等问题。

常见内存开销来源:纹理贴图、网格、音频/视频、动画、材质和 Shader、C# 托管堆、GameObject 及组件。

核心指标

指标 含义 关注点
Total Used Memory 已使用内存总量 最直观的内存压力指标
Texture Memory 贴图内存(含 Mipmap) 通常是最大占用部分
Mesh Memory 网格内存 场景物体数量/模型复杂度
Object Count UnityEngine.Object 实例总数 过高可能资源未卸载
GC Used Memory 托管堆已使用内存 GC 压力来源
GC Allocated In Frame 该帧分配的托管堆内存 频繁跳动说明有 GC 问题

Simple 视图关键字段

  • Total Committed Memory:Unity 请求的总内存(虚拟内存)
  • Tracked Memory:Unity 能追踪的内存(托管堆、图形资源等)
  • Untracked Memory:无法追踪的内存(本机插件、Mono/IL2CPP 元数据)

Audio 模块

作用:分析和监控音频系统的性能表现,包括播放、混音、CPU 占用等。

核心指标

指标 含义 健康值
Playing Audio Sources 正在播放的音频源数量 移动端建议 < 20~30
Audio Voices 使用的音频通道数 若 AudioSource 多但 Voices 少,说明声音被抢占
Total Audio CPU 音频 CPU 使用量 < 10% 健康,> 20% 需排查
Total Audio Memory 音频内存量 持续增长可能资源未释放

Detailed 视图关键字段

  • Priority:播放优先级(0~256,越小越高),高于 128 容易被剔除
  • Virtual:是否因达到 Max Real Voice Count 而被虚拟化(不实际播放)

Video 模块

作用:分析视频播放性能和资源使用情况。

核心指标

指标 含义 问题判断
Playing Video Sources 正在播放的视频源数量 为 0 说明未启动
Software Video Playback 软解码视频数量 不为 0 说明编码格式不被硬件支持,建议改 H.264
Total Frames Dropped 丢帧总数 持续增长说明播放不流畅
Pre-buffered Frames 预缓冲帧数 太少可能导致卡顿
Total Video Memory 视频内存 持续增长可能未释放资源

Physics 模块

作用:监控 3D 物理系统运行状态与性能开销。

核心指标

指标 含义 关注点
Physics Used Memory 物理系统内存 持续上升可能碰撞体未销毁
Active Dynamic Bodies 活动中的动态刚体数量 物理运算主要开销来源
Trigger Overlaps 触发器重叠事件数 避免频繁进入/退出
Physics Queries 物理查询调用次数 频繁查询是性能瓶颈重要来源
Broadphase Adds/Removes 广义阶段添加/移除数 数百以上需关注对象生命周期管理

碰撞检测模式

  • Discreet:离散检测,效率高,适用于大多数物体
  • Continuous:连续检测,精确但开销大,仅对高速移动物体使用

Physics 2D 模块

作用:监控 2D 物理系统(基于 Box2D 引擎)的性能与行为。

核心指标

指标 含义 关注点
Total Contacts 总接触点数 碰撞交互是否过多
Total Queries 物理查询次数 Raycast/Overlap 调用是否频繁
Total Callbacks 物理回调次数 OnCollision/Trigger 回调数量
Continuous Bodies 连续检测刚体数 仅高速物体使用,避免滥用
Physics Used Memory (2D) 2D 物理内存 持续上升说明资源未释放

常见问题排查

  • Total Queries 高:合并检测逻辑、加帧间隔、限制 Layer
  • Continuous Bodies 高:只给高速刚体启用 Continuous 模式
  • Physics Used Memory 持续上升:检查动态创建对象是否被销毁

UI 和 UI Details 模块

作用:分析 UGUI 系统性能,从整体统计和细节绘制层面排查 UI 问题。

核心指标

指标 含义 优化方向
Layout Time 布局系统耗时 LayoutGroup 频繁重算会拖慢主线程
Render Time Canvas 绘制耗时 合批失败、DrawCall 过多
Cumulative Batch Count 总批次数 越少越好,UGUI 性能关键
Batch Breaking Reason 合批失败原因 定位 DrawCall 增加的根本原因

Batch Breaking Reason 常见原因

  • Different Texture:多个 Image 使用不同图集 → 使用 SpriteAtlas 合并
  • Different Material Instance:TMP 修改样式生成新材质 → 统一字体材质
  • Mask/Clipping:Mask、ScrollRect 触发 Canvas 分离 → 减少 Mask 层级

渲染预览视图:可切换 Standard/Overdraw/Content 模式,直观查看每个 Batch 渲染内容,定位 UI 元素重叠绘制问题。

Realtime GI 模块

作用:排查实时全局光照系统的运行时性能开销(需在 Lighting 中开启)。

核心指标

指标 含义 关注点
Solve Tasks 光照核心求解耗时 最耗性能的部分
Dynamic Objects 动态物体光照计算 动态发光物体是否频繁更新
Blocked Command Write 写入阻塞耗时 非 0 说明存在同步瓶颈

优化建议

  • Solve Tasks 每帧耗时过高 → 转为静态光照或减少动态发光物体
  • Dynamic Objects 频繁更新 → 让动态物体远离 GI 区域或使用 Light Probe

Virtual Texturing 模块

作用:监视虚拟纹理(VT)系统的性能开销。VT 将大纹理切成 tile 按需加载,减少显存占用。

适用场景:超大贴图资源、开放世界流式加载、次世代大型项目(仅 HDRP 支持)。

核心指标

指标 含义 问题判断
Required Tiles 当前视图所需的 tile 数量 反映纹理数据需求量
Missing Streaming Tiles 尚未加载的 tile 数量 过高说明流式传输瓶颈
Max Cache Mip Bias 最大 Mip 偏移 越高说明画质越模糊(降级渲染)
Total GPU Cache Size GPU 缓存总量 评估显存压力

优化方向

  • Missing Streaming Tiles 高 → 增大缓存或优化加载策略
  • Max Cache Mip Bias 高 → 显存压力大,系统在降级纹理质量

File Access 模块

作用:监控运行时文件系统访问情况,排查磁盘 I/O 导致的卡顿。

核心指标

指标 含义 问题判断
Files Opened 打开的文件数量 频繁打开可能资源未预加载
File Handles Open 当前打开的文件句柄数 长期偏高可能句柄泄漏
Reads in Flight 进行中的读取操作数 过高说明并发读取过多
File Seeks 文件指针跳转次数 过多说明随机访问,I/O 性能差

排查方向

  • 主线程同步 I/O 耗时超过几 ms → 改用异步加载
  • Files Opened/Closed 不平衡 → 检查文件句柄泄漏
  • File Seeks 过多 → 优化文件访问模式

Asset Loading 模块

作用:监视运行时资源加载行为,配合 File Access 模块定位加载瓶颈。

核心指标

指标 含义 问题判断
Texture Reads 纹理读取量 贴图加载是否过多
Mesh Reads 网格读取量 模型加载开销
Marker Length (ms) 加载耗时 定位加载慢的资源
Thread 加载线程 判断是否阻塞主线程

优化方向

  • 同一帧加载过多资源 → 分帧加载
  • 主线程加载大资源 → 改用异步加载
  • 重复加载同一资源 → 检查 Resources.Load 使用

Profiler 脚本控制 API

命名空间using UnityEngine.Profiling;

常用 API

API 作用
Profiler.GetMonoUsedSizeLong() 获取当前使用的 Mono 堆内存
Profiler.GetMonoHeapSizeLong() 获取 Mono 堆总大小
Profiler.GetTotalAllocatedMemoryLong() 获取 Unity 正在使用的内存总量
Profiler.GetTotalReservedMemoryLong() 获取从操作系统申请的内存总量
Profiler.GetRuntimeMemorySizeLong(obj) 获取某对象占用内存
Profiler.BeginSample("name") / EndSample() 自定义采样区段,在 CPU 模块显示标记

使用建议

  • BeginSample/EndSample#if UNITY_EDITOR 包裹,避免打进正式包
  • logFile + enableBinaryLog 可导出 .raw 文件供离线分析

Memory Profiler 窗口

定位:比 Profiler 的 Memory 模块更细致的内存分析工具,支持快照对比和引用链追踪。

与 Profiler Memory 模块对比

对比项 Profiler - Memory Memory Profiler
实时监控 支持 主要用于快照分析
快照对比 不支持 支持
引用链分析 不支持 支持
性能开销 极小 略高(抓快照时卡顿)

三个核心页签

页签 作用
Summary 内存总体信息,快速分析和横向比较
Unity Objects Unity 引擎对象(Texture、Mesh、Material 等)的内存占用
All Of Memory 最全面的内存视图,按内存类别分类(Native、Managed、Graphics 等)

Summary 页签

四个核心区域

  1. Memory Usage On Device:设备上的内存使用情况,判断是否接近内存上限
  2. Allocated Memory Distribution:已分配内存分布(Native、Managed、Graphics、Untracked)
  3. Managed Heap Utilization:托管堆使用率(Empty Heap、VM、Objects)
  4. Top Unity Objects Categories:内存占用最多的 Unity 对象类型

内存溢出(OOM)参考值

  • Android 低端机:350–450MB
  • Android 高端机:700MB–1.2GB
  • iPhone 12+:1GB+

Unity Objects 页签

核心能力:快速定位 Unity 对象内存占用的类型与具体实例。

三种统计视角

  • Allocated Memory:理论占用的内存量
  • Resident Memory:实际驻留在物理内存中的部分(决定生死)
  • Allocated and Resident:综合视图

关键概念

  • 驻留内存(Resident Memory):真正对设备造成压力,超过上限会 OOM
  • 已分配内存(Allocated Memory):反映健康状况和崩溃风险趋势

常见排查方向

  • 纹理内存过高 → 检查重复加载、未压缩、Read/Write 开启
  • 相同资源多次加载 → Name 相同但 InstanceID 不同
  • 动态资源未释放 → 材质、纹理未及时释放

All Of Memory 页签

定位:最强大、最底层、最全面的内存视图,按内存类别分类。

核心内存类别

类别 含义 排查方向
Native C++ 层对象、资源、图形资源 CPU 端 未及时释放对象
Managed C# 托管堆、类、数组 GC 问题、内存泄漏
Graphics (Estimated) GPU 纹理、Mesh、RT 估算 显存异常、Read/Write 双份内存
Reserved 已申请但未使用的内存 内存碎片化、场景切换后未释放
Untracked 无法追踪的内存 驱动/平台问题,需用平台工具排查

与 Unity Objects 页签区别

  • Unity Objects:按对象类型分类,找哪个资源占用最多
  • All Of Memory:按内存类别分类,分析整体内存健康状况

Unity 机制问题

脚本挂载检测

重要性:继承 MonoBehaviour 的脚本必须挂载到 GameObject 上才会参与运行。

检测方法

  • 在 Hierarchy 搜索框输入 t:脚本名,快速定位挂载该脚本的对象
  • 结合 Profiler 的调用次数和耗时,排查是否因挂载失误导致性能问题

常见问题:脚本误挂到多个对象,导致同一逻辑被多次执行。

生命周期函数注意事项

调用顺序问题

  • 不同脚本的 AwakeStart 执行顺序默认不确定
  • 若 A 依赖 B 的初始化结果,需在 Project Settings → Script Execution Order 中设置顺序
  • 性能排查时不能只看 Update,也要关注 Awake、OnEnable、Start 中的初始化逻辑

空函数开销

  • 空的生命周期函数仍会被 Unity 调用,带来额外开销
  • 特别注意每帧调用的函数:UpdateLateUpdateFixedUpdateOnGUI
  • 不需要时不要声明,删掉逻辑后记得删除函数本身

Debug API 性能消耗

问题Debug.LogDebug.LogError 等会带来额外性能开销,发布版应避免使用。

解决方案

  1. 二次封装 + 开关:封装 Util.Log(),通过 isDebug 开关统一控制
  2. 条件编译
    • [Conditional("UNITY_EDITOR")]:仅编辑器内编译
    • [Conditional("DEVELOPMENT_BUILD")]:仅 Development Build 中编译
public static class Util
{
    private static bool isDebug = true;
    public static void Log(object info)
    {
        if (!isDebug) return;
        Debug.Log(info);
    }
}

垂直同步(VSync)

作用:强制显卡等显示器刷新完成后再提交下一帧,避免画面撕裂。

副作用

  • 输入延迟增加
  • 帧率下降:帧率低于刷新率时会”降档”(如 50 FPS @ 60Hz → 30 FPS)

设置方式

QualitySettings.vSyncCount = 0;  // 关闭
QualitySettings.vSyncCount = 1;  // 每帧同步
QualitySettings.vSyncCount = 2;  // 隔一帧同步

使用建议

  • 开发调试时关闭,观察真实 GPU 瓶颈
  • 低端机卡顿时关闭,减少帧率锁死卡顿
  • 高帧率项目可开启防撕裂,低帧率项目可关闭避免降档
  • 可在设置菜单中暴露 VSync 选项让玩家选择

排除其他应用程序影响

可能干扰因素

  • 系统资源争用:后台应用占用 CPU/GPU/内存/磁盘 IO
  • 输入系统干扰:某些程序劫持键盘鼠标
  • 音视频干扰:其他播放程序抢占资源
  • 文件访问干扰:杀毒、网盘同步锁定文件
  • 远程调试干扰:远程桌面、投屏影响 GPU 和输入

建议

  • 性能分析时关闭一切后台应用
  • 使用独立进程 Profiler(Standalone Process)降低编辑器干扰
  • 保持测试环境纯净,准确定位游戏自身问题

CPU 脚本优化

计时判断代码执行效率

为什么需要手动计时:虽然 Profiler 能排查耗时问题,但手动计时仍有价值——发布版可用于日志上报、自动分析;可做微秒级、细颗粒度比较;可批量测试、计算平均/最大耗时。

计时基本原理

耗时 = 结束时间 - 开始时间

测试建议

  • 多次执行取平均值
  • 不在初始化阶段测,等消耗稳定后触发
  • 不在测试循环内使用 Debug.Log

Unity Time 类计时

APITime.realtimeSinceStartup(不受 Time.timeScale 影响)

float startTime = Time.realtimeSinceStartup;
// 执行逻辑
float spendTime = (Time.realtimeSinceStartup - startTime) * 1000f;  // 毫秒

精度:毫秒级,适合一般耗时统计。

C# Stopwatch 类计时

命名空间System.Diagnostics

常用 API

API 说明
Stopwatch.StartNew() 创建并开始计时
Stop() 停止计时
ElapsedMilliseconds 耗时(毫秒,long)
Elapsed.TotalMilliseconds 耗时(毫秒,double,微秒级)
Restart() 清零并重新开始
Stopwatch sw = Stopwatch.StartNew();
// 执行逻辑
sw.Stop();
double ms = sw.Elapsed.TotalMilliseconds;

精度:微秒级,比 Time 类更精确。

计时类封装(IDisposable + using)

利用 C# 的 using 语句块 + IDisposable,可自动调用 Dispose 计算耗时:

public class CustomTimer : IDisposable
{
    private string _name;
    private uint _num;
    private Stopwatch _watch;

    public CustomTimer(string name, uint num)
    {
        _name = name;
        _num = num == 0 ? 1 : num;
        _watch = Stopwatch.StartNew();
    }

    public void Dispose()
    {
        _watch.Stop();
        double spendTime = _watch.Elapsed.TotalMilliseconds;
        Debug.Log($"{_name} 共耗时{spendTime}ms,平均每次{spendTime/_num}ms");
    }
}

// 使用方式
using (new CustomTimer("测试", 10000))
{
    for (int i = 0; i < 10000; i++) { /* 逻辑 */ }
}

组件获取与 Update

GetComponent 优化

三种重载方式性能对比

方式 示例 性能
泛型 GetComponent<T>() 最快(推荐)
Type GetComponent(typeof(T)) 中等
字符串 GetComponent("TypeName") 最慢

最佳实践

  • 优先使用泛型方式
  • 常用组件在 Awake/Start 中获取并缓存
  • 避免在 Update 中重复调用

Update 函数优化

七大优化手段

手段 说明
移除空 Update 空函数仍会被每帧调用
缓存组件引用 不在 Update 中 GetComponent、Find
降低执行频率 用计时器控制复杂逻辑间隔
减少复杂计算 不变结果初始化时算好
事件驱动 用观察者模式代替每帧轮询
分帧执行 大批量对象每帧只处理一部分
统一管理器 一个 Update 驱动所有回调,减少跨语言开销

降低执行频率示例

private float timer;
void Update()
{
    timer += Time.deltaTime;
    if (timer >= 0.25f)
    {
        ComplexLogic();
        timer = 0;
    }
}

协程与延迟函数

协程开销分析

额外开销

  • 启动成本约为普通函数调用的 3 倍
  • 会额外分配内存保存状态
  • 每次 yield 都有调度开销
  • 大量并发协程带来 GC 压力

协程 vs Update 计时器

对比项 协程 Update 计时器
可读性 好,逻辑集中 一般,需手动管理
性能 有额外开销 无额外开销
适用场景 复杂异步流程 简单定时逻辑

WaitForSeconds vs WaitForSecondsRealtime

  • WaitForSeconds:受 Time.timeScale 影响
  • WaitForSecondsRealtime:不受影响,按真实时间

替代方案:UniTask(async/await)性能更优。

延迟函数优化

Invoke/InvokeRepeating 问题

  • 通过字符串反射调用,性能差
  • 可读性差,无法传参
  • 停止需 CancelInvoke 传字符串

推荐替代方案

  1. 协程 + WaitForSeconds
  2. UniTask.Delay + async/await
  3. 自定义 Timer 管理器

本机托管桥接

概念与开销

本机-托管桥接:C#(托管)与 C++(本机)之间的调用通道。

常见触发桥接的操作

  • transform.position(读/写)
  • GetComponent<T>()
  • gameObject.SetActive()
  • GameObject.Find()

开销来源

  1. 上下文切换:堆栈转换、线程状态保存
  2. 数据封送:Vector3 等结构体拷贝
  3. GC 压力:封送可能产生临时对象

优化方法

方法 说明
缓存组件引用 避免重复 GetComponent
Transform 批量写回 先读到局部变量,算完再一次性写回
子物体引用缓存 初始化时缓存,不用 GetChild 遍历
逻辑与表现分离 托管侧计算完再批量同步
对象池 减少 Instantiate/AddComponent 的桥接开销

Transform 属性优化示例

// 推荐:读一次 → 托管侧计算 → 写回一次
Vector3 pos = transform.position;
pos += moveDir * speed * Time.deltaTime;
transform.position = pos;

// 优先用本地属性
transform.localPosition += offset;

GameObject 与 Transform

GameObject 优化

陷阱 问题 优化方案
gameObject.tag 每次访问产生桥接 缓存或用 CompareTag()
GameObject.Find() 遍历场景,性能差 初始化时查找并缓存
SendMessage() 字符串反射,效率低 缓存组件直接调用或事件中心

Transform 优化

陷阱 问题 优化方案
SetParent() 触发层级重建、批处理中断 避免运行时频繁修改
频繁改 position 触发矩阵运算、通知其他组件 缓存后一次性写回
用世界属性 层级越深计算量越大 优先用 localPosition
transform.Find() 遍历层级,性能差 初始化时缓存引用

数据结构与缓存

数据结构选择

数据结构 查找复杂度 特点 适用场景
List<T> O(n) 连续存储,缓存友好 频繁遍历、中间插入少
Dictionary<K,V> O(1) 哈希表,不连续 频繁按键查找
HashSet<T> O(1) 无序、无键值对 只关心存在性
Queue<T> O(1) 先进先出 对象池、按序处理
Stack<T> O(1) 先进后出 状态回退、行为撤销
LinkedList<T> 中间插删 O(1) 内存不连续 频繁中间插删、少遍历

选择原则:遍历多用 List,按键查找用 Dictionary,存在性检测用 HashSet。

复杂计算结果缓存

适合缓存的内容

  • 数学计算:三角函数预计算查表(0~359 度 Sin/Cos)
  • 寻路结果:缓存起点+终点对应路径
  • 配置数据:反序列化后常驻内存
  • 物理投射:条件不变时复用 Raycast/Overlap 结果
  • Shader 参数:仅在变化时调用 SetFloat/SetVector

核心思想:用内存换性能,避免重复计算。

禁用未使用对象

OnBecameVisible / OnBecameInvisible

回调 触发条件 用途
OnBecameVisible Renderer 对任意摄像机可见 启用脚本/对象
OnBecameInvisible Renderer 对所有摄像机不可见 禁用脚本/对象

注意:Scene 视图摄像机也会触发;对象必须有 Renderer 组件。

距离判断优化

// 推荐:用平方比较,避免开方
if ((posA - posB).sqrMagnitude >= threshold * threshold)
{
    // 超过阈值,禁用对象
}

视锥范围检测

Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
if (GeometryUtility.TestPlanesAABB(planes, renderer.bounds))
{
    // 在视锥内
}

资源加载与预渲染

资源加载开销来源

开销类型 说明
I/O 开销 磁盘/网络读取阻塞 CPU
解析开销 数据解析、解压(AB 包 LZ4/LZMA)、反序列化
内存分配 创建 C++ 引擎对象(Mesh、Texture 等)

优化策略

策略 说明
减小序列化对象 预设体精简、二进制替代文本配置
公共数据配置化 全局只保留一份配置表,通过 ID 查数据
异步加载 + 分帧实例化 避免主线程阻塞,每帧实例化少量对象
缓存 资源字典缓存、对象池复用
预热 预加载、预实例化、预渲染

预渲染方式

方式 适用场景
隐藏相机 + RenderTexture 动画、粒子等多帧预渲染
CommandBuffer 仅需上传网格、纹理、Shader 变体
Shader Variant Collection.WarmUp() Shader 变体预热

CPU 图形优化

DrawCall 与批处理

DrawCall 开销来源

每次 DrawCall 的 CPU 开销:

  1. 状态切换:绑定材质、贴图、Shader
  2. 数据绑定:上传模型矩阵、材质参数
  3. 命令组织:调用图形 API 生成命令缓冲
  4. 排序剔除:为减少状态切换做排序

增加 DrawCall 的常见原因

  • 材质/Shader 不一致
  • 相同材质但纹理不同
  • 不同渲染队列
  • 不同透明度(透明队列按深度排序)
  • 不同 Shader 变体
  • UI 元素位于不同 Canvas
  • Shader 多 Pass(阴影、深度、后处理)

批处理方案对比

方案 原理 限制 适用场景
动态批处理 运行时合并网格 顶点 ≤225(300),属性 ≤900,相同材质 小型动态网格,内置管线
静态批处理 构建/首次运行时合并 勾选 Batching Static,相同材质 静态场景物体
GPU Instancing 一次 Draw Call 绘制多个实例 相同网格+相同材质 大量相同物体(草地、树木)
SRP Batcher 缓存材质/物体数据到 GPU 仅 SRP 管线,Shader 兼容 URP/HDRP 项目

优先级

  • 内置管线:静态批处理 > GPU Instancing > 动态批处理
  • SRP 管线:静态批处理 > SRP Batcher > GPU Instancing

动态批处理限制

  • 顶点数限制:≤225(Unity 2021 前为 ≤300)
  • 顶点属性限制:顶点数 × 属性数 ≤ 900
  • 必须相同材质实例
  • 不支持 SkinnedMeshRenderer
  • 变换不能包含镜像(正/负缩放不能混批)
  • 相同光照状态、渲染队列

静态批处理限制

  • 必须勾选 Batching Static
  • 相同材质、相同渲染状态
  • 每批次最多 64,000 顶点
  • 运行时不能修改 Transform
  • 内存开销翻倍(保留原始网格 + 合并网格)

GPU Instancing 限制

  • 必须相同网格
  • 必须相同材质(颜色等属性可通过实例数据变化)
  • 材质勾选 Enable GPU Instancing
  • 不支持 SkinnedMeshRenderer

SRP Batcher 限制

  • 仅 SRP 管线(URP/HDRP)
  • Shader 必须兼容(Lit/Unlit Shader 支持)
  • 不能使用 MaterialPropertyBlock
  • 不支持粒子

手动合并网格

核心方法

Mesh.CombineMeshes(CombineInstance[], mergeSubMeshes, useMatrices)

合并流程

  1. 获取要合并的网格数据
  2. 构建 CombineInstance 数组(设置 mesh、transform)
  3. 创建新网格,设置顶点索引格式(UInt16/UInt32)
  4. 调用 CombineMeshes 执行合并
  5. 重新计算包围盒 RecalculateBounds()
  6. 挂载合并后的网格

变换矩阵

// 子网格顶点从子对象本地空间转换到父对象本地空间
combineInstance.transform = parentTransform.worldToLocalMatrix * childTransform.localToWorldMatrix;

顶点索引格式

格式 最大顶点数
UInt16 65,535
UInt32 4,294,967,295

CPU 物理优化

Physics Debugger

五大功能区

区域 作用
Info 显示选中刚体的运行时物理属性(速度、质心、休眠状态等)
Filtering 控制哪些物理对象在 Scene 视图中显示
Rendering 调整调试可视化的颜色、透明度、显示方式
Contacts 显示物体之间的接触点、冲量、分离向量
Queries 可视化射线检测、范围检测

用途:调试碰撞不触发、穿模、弹力异常、射线未命中等问题。

时间步长设置

固定时间步长(Fixed Timestep)

参数 默认值 说明
Fixed Timestep 0.02s (50Hz) 物理更新间隔,增大可降低 CPU 开销但影响精度

物理更新独立于帧率:一帧可能执行 0 次、1 次或多次物理更新。

最大允许时间步长(Maximum Allowed Timestep)

参数 默认值 说明
Maximum Allowed Timestep 0.333s 掉帧时最多补多少物理帧,超出的时间丢弃

注意:改小该值会减少物理计算,但可能导致物理对象瞬移、跳跃。

碰撞层矩阵

碰撞检测两阶段

阶段 目的 方法
Broadphase(粗检测) 快速筛选可能碰撞的候选对 AABB 边界盒检测
Narrowphase(精检测) 精确计算接触点 实际碰撞体形状计算

Layer Collision Matrix

  • 勾选:两个层的碰撞器进行碰撞检测
  • 取消勾选:两个层完全不进入 Broadphase,节约 CPU

优化思路:让无关层互斥,减少无效碰撞候选对。

碰撞器性能

性能消耗排行(从低到高)

排名 碰撞器类型 特点
1 Sphere Collider 数学公式简单,最省性能
2 Capsule Collider 数学公式计算,常用于角色
3 Box Collider 矩阵与边界检测,使用最广
4 组合碰撞体 多个原始体拼接,远低于 Mesh
5 Terrain Collider 高度图数据,大规模场景优化
6 Mesh Collider(凸形) 转为凸包,比凹形高效
7 Mesh Collider(凹形) 逐三角面检测,性能最差

原则:尽量用原始碰撞体,避免 Mesh Collider。

碰撞器组合消耗

组合 消耗
相同简单碰撞体(Sphere-Sphere 等) 最低
不同简单碰撞体(Sphere-Box 等) 略高,差别不大
简单碰撞体 + Mesh Collider 取决于网格复杂度,指数级增加
Mesh Collider + Mesh Collider 取决于网格复杂度,指数级增加

刚体优化

碰撞检测类型选择

类型 原理 开销 适用场景
Discrete 固定步长采样点检测 最低 绝大多数常速物体
Continuous 两帧间扫掠检测 中等 高速投射物(子弹)
Continuous Dynamic 更严格的连续检测 最高 关键高速物体
Continuous Speculative 预测式检测 中等 中高速物体,可能误判

首选 Discrete,仅高速物体用 Continuous 系列。

刚体 API 调用原则

  • 在 FixedUpdate 中调用MovePositionMoveRotationAddForce、修改 velocity
  • 不要在 Update 中直接改 Transform:会触发强制同步、AABB 重建、接触点重建

岛屿效应

  • 岛屿:通过碰撞接触或关节相连的刚体组成独立解算单元
  • 休眠:岛屿内所有刚体满足休眠条件时整体休眠
  • 唤醒:岛屿内一个刚体被唤醒,整个岛屿都唤醒

优化方向

  • 减少关节绑定
  • 用运动学刚体切断岛屿
  • 利用碰撞层矩阵过滤无效碰撞

碰撞检测优化

射线检测 API 选择

API 特点
RaycastAll 每次分配新数组,产生 GC
RaycastNonAlloc 写入传入数组,无 GC(推荐)

Broadphase 重建触发条件

  • 移动静态碰撞体
  • 在 Update 中直接改刚体 Transform
  • 修改 Collider 尺寸
  • 启用/禁用 Collider
  • 大量 Instantiate/Destroy

避免上述操作可减少 CPU 峰值。

布娃娃优化

  • 减少碰撞器数量(约 7 个即可)
  • 关闭布娃娃层与自身的碰撞
  • 及时禁用不活跃的布娃娃

GPU 优化

影响因素

填充率(Fill Rate)

概念:GPU 单位时间内能处理的像素数量(GPixel/s)。

造成瓶颈的原因

因素 说明
高分辨率 像素数指数增加
全屏后处理 每像素多次采样计算
MSAA 4x MSAA 填充率翻倍
透明/半透明物体 每层都要计算片元
多 Pass Shader 同一像素多次覆盖绘制

优化思路

  • 降低分辨率(动态分辨率、注视点渲染)
  • 减少屏幕后处理效果
  • 降低抗锯齿采样次数
  • 减少半透明使用

OverDraw(过度绘制)

概念:同一屏幕像素在一帧中被重复绘制多次。

影响性能的原因

  • 片元着色器重复执行
  • 带宽浪费
  • 填充率压力

优化思路

对象类型 优化方案
不透明物体 从前向后渲染、遮挡剔除
半透明物体 减少层数、Alpha Test 替代 Alpha Blend
UI 系统 RectMask2D 替代全屏 Mask、减少叠层

Early-Z:片元着色器运行前先做深度测试,被遮挡的直接丢弃。

内存带宽

概念:GPU 与显存之间单位时间的数据传输量(GB/s)。

造成瓶颈的原因

  • 高分辨率 + 高刷新率
  • 超高分辨率纹理
  • 缺少 Mipmap
  • 未压缩纹理格式
  • RenderTarget 过大/过多

优化思路

  • 使用 Mipmap + 纹理压缩(ASTC/ETC2)
  • 贴图合并减少采样次数
  • 降低 RT 精度
  • 半分辨率渲染后处理

顶点与片元

| 类型 | 主要开销 | 优化方向 |
| — | — |
| 顶点 | 模型顶点数、骨骼蒙皮、顶点着色器复杂度 | LOD、网格简化、蒙皮优化 |
| 片元 | 分辨率、OverDraw、片元着色器复杂度 | 降低分辨率、减少采样、避免分支 |

顶点优化

  • LOD 切换不同精度模型
  • 减少骨骼数量和顶点权重
  • 顶点动画烘焙到纹理(VAT)
  • 顶点着色器输出精简

片元优化

  • 减少纹理采样次数
  • 避免 Shader 分支(用 lerp/step 替代 if/else)
  • 减少全屏 Pass
  • 避免 Early-Z 失效

缓存与显存

存储类型 特点
显存 容量大(GB级)、带宽高、延迟大
缓存 容量小(KB~MB级)、速度快

缓存优化

  • 提高局部性:UV 连续、合并图集
  • 减少缓存未命中:降低 OverDraw、避免依赖采样
  • 数据轻量化:用 half 替代 float

显存优化

  • 资源压缩:纹理压缩、法线压缩
  • 降低资源规格:贴图分辨率、RT 数量
  • Mipmap + Streaming

并行度与调度

并行度不足:GPU 核心空转,算力浪费。

  • 小批量 DrawCall
  • Shader 分支导致线程分化

调度问题:等待内存时 ALU 闲置。

优化思路

  • 提高批处理力度
  • 避免 Warp 内线程分化(减少 if/else)
  • 控制寄存器压力
  • 提高缓存命中率

增强渲染性能

GPU Skinning

模式 说明 适用场景
CPU 蒙皮在 CPU 计算 动画较少的项目
GPU 蒙皮在 GPU 顶点着色器 大量角色、高顶点模型
GPU (Batched) 多个 SMR 合并在一次 GPU Pass 大量相同角色(群怪)

选择建议:中高端设备优先 GPU (Batched),低端设备用 GPU 更稳。

LOD(细节层次)

作用:根据距离切换不同精度模型,减少远处物体顶点数。

Fade Mode

模式 特点
None 直接切换,性能最好,可能跳变
Cross Fade 渐变过渡,平滑但 GPU 负担增加
Speed Tree 植被专用,仅 SpeedTree 资源

切换阈值:基于物体包围盒在屏幕上的占比。

遮挡剔除(Occlusion Culling)

作用:剔除视锥内被遮挡的物体,弥补视锥剔除的局限。

使用步骤

  1. 设置遮挡物(Occluder Static)和被遮挡物(Occludee Static)
  2. 在 Occlusion Culling 窗口烘焙
  3. 摄像机开启遮挡剔除

烘焙参数

参数 说明
Smallest Occluder 小于此尺寸的物体不作为遮挡者
Smallest Hole 小于此尺寸的孔洞会被填上
Backface Threshold 背面参与遮挡的比例

适用场景:室内、城市、森林等遮挡多的环境。

降低几何复杂度

方法 说明
美术优化 少用高面数模型,用法线贴图代替细节
移除网格 删除不重要的装饰几何体
LOD 根据距离切换精度
遮挡剔除 剔除被遮挡物体
Impostor 远处用 2D 贴图冒充 3D 模型
网格简化 使用算法减少面数

曲面细分

概念:将低多边形网格自动拆分成更多小三角形。

性能影响

  • 顶点处理压力成倍增加
  • 片元处理间接增加
  • 内存带宽开销上升
  • 缓存效率下降

建议:无特殊需求时避免使用,尤其低端设备。

UI 系统优化

Canvas 组件优化

Canvas 重建机制:Canvas 内任意元素变化都会触发整个 Canvas 的网格重建。

动静分离原则

类型 特点 处理方式
静态元素 永不改变(背景、Logo、装饰线) 放入独立 Canvas
偶尔动态 偶尔响应变化(按钮、进度条) 放入独立 Canvas
持续动态 每帧变化(血条动画、计时器) 放入独立 Canvas

核心思想:高频变化区域独立 Canvas,避免小元素更新导致大 Canvas 重建。

RaycastTarget 优化

作用:控制 UI 元素是否参与射线检测。

禁用原则

必须禁用 必须保留
背景图、装饰图标、分割线 Button、Toggle、Slider
阴影、UI 框架图 ScrollRect 可交互区域
纯装饰性、说明性文字 需要响应点击的元素

性能收益:减少射线检测遍历次数,降低 UI 交互开销。

Canvas 禁用与 Animator 避免

隐藏 UI 的正确方式

  • 禁用 Canvas 组件(而非 GameObject),避免重建开销
  • 禁用 Canvas 不会销毁网格数据,重新启用时无需重建

避免 Animator

  • Animator 每帧都会检查属性变化,即使动画未播放
  • UI 动画优先使用 DOTween、Tween 或代码控制
  • 必须用 Animator 时,动画结束后禁用 Animator 组件

世界空间 Canvas 的相机定义

问题:World Space Canvas 不指定 Event Camera 时,射线检测会遍历所有相机。

优化:为每个 World Space Canvas 指定专属 Event Camera,减少射线检测范围。

隐藏 UI 元素的优化

错误方式:设置 Alpha = 0 或 Scale = 0

  • 元素仍参与布局计算和绘制
  • 产生不必要的 OverDraw

正确方式

  • 禁用 Canvas 组件(整体隐藏)
  • 禁用 Graphic 组件(单个元素隐藏)
  • 移出 Canvas 范围(临时隐藏)

ScrollRect 优化

性能问题:大量子元素导致布局计算、绘制开销大。

核心优化思想

  1. 虚拟列表:只保留可见范围内的元素,利用缓存池复用
  2. 自定义布局:不使用 Unity 自带 LayoutGroup,自己控制布局逻辑
  3. 独立 Canvas:ScrollRect 内容放入独立 Canvas,动静分离

实现要点

  • 监听 ScrollRect 的 onValueChanged 事件
  • 动态计算哪些元素应该显示
  • 从缓存池获取/回收元素

Text 拦截全屏 UI 交互

场景:全屏 UI 背后游戏画面仍响应点击。

解决方案

  • 在全屏 UI 最底层放置一个 Text 组件
  • 设置 RaycastTarget = true
  • 文本内容为空,不显示任何内容

原理:Text 的射线检测会拦截点击事件,阻止穿透到游戏场景。

其他 UI 优化方案

方案 说明
打图集 多个 UI 元素共用同一材质,便于合批;不同图集元素分层显示避免打断合批
少用布局组件 ContentSizeFitter、LayoutGroup 等自带布局组件开销大,建议自己实现布局逻辑
关闭 Mipmap 非 3D UI 元素关闭 Mipmap,减少显存和采样开销
图集压缩 移动端使用 ASTC 或 ETC2,降低内存占用
九宫格拉伸 仅需要时启用,避免不必要计算
减少 OverDraw 合并背景层、避免大面积半透明遮罩

粒子系统优化

CPU 侧优化

优化目标:少算粒子——减少每帧处理的粒子数,以及每个粒子要计算的内容。

减少粒子并发量

参数 作用
Start Lifetime 缩短生命时间,降低同时存活粒子数
Rate over Time 控制发射频率,避免单位时间粒子过多
Max Particles 限制最大粒子数,防止峰值无限增长
LOD/距离剔除 远处降低发射率或关闭

减少高开销模块

模块 开销原因 优化建议
Collision CPU 逐粒子碰撞检测 能不用就不用;必须用时用简单碰撞体、关闭 Send Collision Messages
Triggers 逐粒子检测 + 频繁回调 严格限制触发条件
Noise 每帧采样噪声函数 降低频率/强度,或用随机值一次性赋值
Trails 额外顶点,指数增长 调大 Minimum Vertex Distance,限制最大轨迹长度
Sub Emitters 粒子数指数增长 尽量不用,或只在关键时机触发

减少脚本和排序开销

  • 少用 OnParticleCollisionOnParticleTrigger 回调(可能被大量粒子反复调用)
  • 少用 GetParticles/SetParticles(涉及托管与原生数据拷贝,不要每帧调用)
  • 半透明粒子需要排序,能不透明就不透明

利用剔除和对象池

  • Simulation Space 设为 Local 更容易整体剔除
  • Stop Action 设为 Disable 或 Destroy,避免播完后后台模拟
  • 频繁使用的粒子(技能特效、受击特效)做对象池化

减少 DrawCall:多个粒子系统共用材质或图集,开启 GPU Instancing。

GPU 侧优化

优化目标:少算像素——减少屏幕上参与计算的像素数,以及每个像素计算的内容。

控制 OverDraw

方法 说明
减少粒子数量和尺寸 覆盖面积 ≈ 粒子数 × 粒子尺寸
Trim Alpha 裁掉贴图边缘无效透明区域
合理 Start Size 避免大面积满屏效果
LOD 远处用雾面贴图或后处理替代

降低几何复杂度

  • 使用广告牌粒子(一个四边形即可)
  • 合理使用 Trails(调大采样间距、限制最大顶点数)
  • 网格粒子用低模

减少片元着色开销

  • 降低着色器复杂度:减少纹理采样、避免分支和高开销函数
  • 粒子一般不必投射阴影,光照需谨慎
  • 优化贴图:减少序列帧数量、降低分辨率、使用压缩格式

替代手段

  • 能用不透明就不用半透明(弹壳、碎屑等)
  • 大范围雾、火焰用后处理替代
  • 上万粒子考虑 VFX Graph(仅 URP/HDRP)

着色器优化

优化必要性

  • Shader Graph、ASE 等可视化工具生成的 Shader 往往不是最优
  • 为适配各种情况会生成通用代码,可能包含冗余计算
  • 手写 Shader 可做指令级优化

降低计算量

方法 说明
使用移动平台 Shader Unity 内置移动端 Shader,无需改代码即可提升性能
使用小数据类型 移动端优先 halffixed,避免频繁精度转换
使用 GPU 优化辅助函数 Unity 内置 Shader 库函数,跨平台兼容且高效
删除不必要输入数据 顶点/片元结构体中不用的字段不要声明
只公开所需变量 固定值直接写死,减少常量缓冲区传输
预计算 光照烘焙、LUT 纹理,避免每帧重复计算
减少逐像素计算 能在顶点算的就不放片元
合并多次计算 避免重复计算同一表达式,存结果复用
利用常量缓冲区 共用常量放 CBUFFER,一次传输多处使用

减少采样开销

方法 说明
减少纹理采样 多张灰度图合并到一张 RGBA 的各通道
使用更少纹理数据 遮罩贴图用 R8 代替 RGBA32
测试压缩格式 PC 用 BC,Android 用 ETC2/ASTC,iOS 用 PVRTC/ASTC
最小化纹理交换 降低分辨率、复用纹理、使用图集
控制 VRAM 移动端贴图总占用控制在 500MB 以内
使用 Mipmap 远处物体用低分辨率 Mip,降低带宽和闪烁
采样复用 一次采样结果多次使用,避免重复采样
避免动态分支选纹理 用纹理图集、Texture2DArray、Shader 变体替代
压缩法线纹理 只存 RG 两通道,Shader 重建 Z,减少 50%~75% 存储
流式加载 虚拟纹理、Streaming Mipmap,按需加载

降低填充率压力

三类思路

  1. 降低要处理的像素数量:动态分辨率、降低后处理、降低 MSAA
  2. 减少单像素计算量:控制 Pass 数、减少半透明、合理光照模式
  3. 避免无效像素计算:减少 OverDraw、Occlusion Culling

具体方法

方法 说明
禁用不需要的特性 ForceNoShadowCastingZWrite Off
Shader LOD 远处用简单 SubShader,降低片元计算
光照剔除 Culling Mask 限制受光层级
谨慎实时阴影 用烘焙阴影替代,降低阴影质量
烘焙光照纹理 静态物体用光照贴图,动态物体用 Light Probe
减少 MRT 多重渲染目标增加填充率压力,移动端谨慎使用

阴影质量设置

内置管线

  • Quality 窗口:Shadowmask Mode、Shadow Resolution、Shadow Cascades 等
  • Light 组件:Shadow Type、Realtime Shadows 参数
  • Renderer 组件:Cast Shadows、Receive Shadows

URP 管线

  • URP Asset:Max Distance、Cascade Count、Soft Shadows Quality
  • Light 组件:Shadow Type、Custom Shadow Layers
  • Renderer 组件:Static Shadow Caster(静态阴影投射体)

建议:移动端用 Low/Medium 分辨率、No 或 Two Cascades;PC/主机用 High、Four Cascades。

光照烘焙

概念:提前离线计算光照、阴影、反射、间接光照,保存为贴图或探针数据,运行时直接使用。

可烘焙:直接光照、间接光照、静态阴影、反射(Reflection Probe)
不可烘焙:随时间变化的灯光或移动的光源

烘焙步骤

  1. 静态物体标记 Static 或勾选 Contribute GI
  2. 设置灯光模式:主光源 Mixed、静态灯 Baked、动态灯 Realtime
  3. Lighting 窗口配置参数,点击 Generate Lighting

混合光照模式

模式 特点 适用场景
Shadowmask 烘焙静态阴影 + 实时动态阴影 主机/PC,静态建筑 + 动态角色
Subtractive 只允许一个主方向光实时阴影 移动端、卡通风、简单关卡
Baked Indirect 只烘焙间接光,直接光和阴影实时 室外场景、昼夜循环

光照探针

作用:让动态物体在场景中通过插值获得周围环境的间接光照信息。光照烘焙为静态物体服务,动态物体无法直接使用烘焙光照,Light Probe 解决了这个问题。

原理

  • 烘焙阶段:在场景中放置探针点,Unity 对每个探针计算光照方向和强度分布,存储为球谐函数(SH)系数
  • 运行阶段:对每个动态物体,Unity 找到附近的光照探针,根据位置进行三维插值,将结果应用到物体上

优点:几乎零运行时成本、光照平滑过渡、与烘焙光照无缝结合
缺点:无法产生阴影、光照数据静态、探针过少时亮度跳变

使用步骤

  1. 创建 Light Probe Group,编辑探针位置
  2. 重新烘焙(Generate Lighting)
  3. 动态物体 Renderer → Light Probes 设为 Blend Probes

布局原则:覆盖动态物体活动区域、光照变化剧烈处密集、空间分布均匀连续

注意:Light Probe 只烘焙间接光,直接光需光源设为 Mixed 或 Realtime。

反射探针

作用:捕捉某个空间内的环境反射信息,生成 Cubemap,让金属、玻璃等材质获得倒影效果。

原理

  • 烘焙阶段:以探针位置为中心渲染场景 6 个方向,生成 Cubemap
  • 运行阶段:Shader 根据法线方向在 Cubemap 中查找反射颜色
  • 多探针时按位置权重插值混合

三种类型

类型 特点 适用场景
Baked 只在烘焙时生成,运行时不更新 静态场景、室内、固定反射环境
Realtime 运行中定时或每帧重新渲染 镜面、水面、动态光照变化
Custom 手动指定 Cubemap 特殊效果或自定义反射

关键参数

  • Importance:探针优先级
  • Intensity:反射亮度倍数
  • Box Projection:启用盒体投影,室内更真实
  • Blend Distance:探针间混合过渡距离
  • Resolution:Cubemap 分辨率

使用步骤

  1. 创建 Reflection Probe,设置参数
  2. 静态物体勾选 Reflection Probe Static 后烘焙
  3. URP 在管线资源启用 Probe Blending;内置管线在 Renderer 设 Blend Probes

布局技巧:室内居中、室外间隔、走廊多段、每层独立。

GPU 并行特性优化思路

核心思想:把计算分成简单、独立、可并行的工作单元,让每个 GPU 线程做少量规则的工作。

向量化运算:GPU ALU 以 4-wide SIMD 工作,应使用向量运算而非分量逐个计算。

避免线程发散:warp/wavefront 内所有线程执行相同指令,动态分支导致线程闲置。用 saturate/lerp 替代 if,或用 Shader 关键字生成变体。

内存访问合并:连续内存访问可被合并为更少事务,随机访问严重拉低带宽效率。

预计算并行查找表:离线预计算复杂函数结果存入纹理/数组,运行时查表替代实时计算。

批处理:合并 Draw Call,让 GPU 在更大批次中并行工作。

减少昂贵的原子操作:原子操作在高并发下串行化同一地址访问,避免大量线程写同一计数器。

减少数据依赖:让每个线程独立计算,减少顺序/循环/帧间依赖,依赖逻辑移到 CPU。

ComputeShader 基础

作用:在 GPU 上运行的通用计算程序(GPGPU),脱离渲染管线,直接利用 GPU 并行计算能力处理任意类型数据。

与普通 Shader 区别:普通 Shader 依赖渲染管线用于渲染画面;Compute Shader 不依赖渲染管线,专门用于计算数据。

基本结构

#pragma kernel CSMain        // 声明入口函数
RWTexture2D<float4> Result; // 声明输入输出资源
[numthreads(8,8,1)]         // 声明线程组内线程数量
void CSMain(uint3 id : SV_DispatchThreadID) { ... }

C# 调用流程

  1. FindKernel("入口函数名") 获取内核索引
  2. SetFloat/SetVector/SetBuffer/SetTexture 传递参数
  3. Dispatch(kernel, x, y, z) 启动计算
  4. GetData/AsyncGPUReadback 获取结果
  5. Release() 释放资源

执行模型三层结构

  • 线程:GPU 并行执行的最小计算单元
  • 线程组:由 [numthreads(x,y,z)] 定义,共享组内内存
  • 线程组网格:C# 侧 Dispatch() 定义,控制全局总线程数

总线程数计算总线程数 = numthreads(x,y,z) × Dispatch(gx,gy,gz)

ComputeShader 数据类型

标量类型:bool(1byte)、int(4byte)、uint(4byte)、float(4byte)、double(8byte)
向量类型:float2/float3/float4、int2/int3/int4、uint2/uint3/uint4
矩阵类型:float2x2、float3x3、float4x4
复合类型:struct、StructuredBuffer(只读)、RWStructuredBuffer(可读写)、ByteAddressBuffer、AppendStructuredBuffer、ConsumeStructuredBuffer
纹理类型:Texture2D(只读)、RWTexture2D(可读写)、TextureCube、Texture2DMS
常量缓冲区:cbuffer,按块上传更高效

注意:不建议使用 half,存在对齐问题、平台支持问题。

ComputeShader 参数传递

标量SetFloatSetIntSetBool
向量SetVector(建议非 4 维用 SetFloats/SetInts
矩阵SetMatrix
缓冲区ComputeBuffer + SetData + SetBuffer
纹理SetTexture(RWTexture 需 enableRandomWrite = true 的 RenderTexture)
常量缓冲区SetConstantBuffer

ComputeBuffer 创建new ComputeBuffer(count, stride)new ComputeBuffer(count, stride, ComputeBufferType.Append)

ComputeShader 三大参数语义

语义 含义 说明
SV_DispatchThreadID 全局线程 ID 每个线程在整个 Dispatch 中的唯一索引,可直接映射到数据下标
SV_GroupThreadID 组内线程 ID 当前线程在组内的局部索引
SV_GroupID 线程组 ID 当前线程组在整个 Dispatch 网格中的位置

关系SV_DispatchThreadID = SV_GroupID × numthreads + SV_GroupThreadID

最常用:SV_DispatchThreadID,直接映射到图像像素或数组元素下标。

ComputeShader 线程组配置

numthreads 规则

  • 每个维度 >= 1 且 <= 1024
  • x × y × z <= 1024

Dispatch 规则

  • 每个维度 >= 1
  • 无硬性上限

匹配规则groupsX = CeilToInt(width / numThreadX)

推荐配置

数据维度 推荐 numthreads
1D 数据 (64,1,1) 或 (128,1,1)
2D 数据 (8,8,1) 或 (16,16,1)
3D 数据 (4,4,4) 或 (8,8,8)

原因:GPU 线程调度单位是 Warp/Wavefront(32 或 64 个线程),最好让 x×y×z 是 32 或 64 的倍数。

ComputeShader 数据获取

同步获取

  • ComputeBuffer:GetData(array) 阻塞 CPU
  • RenderTexture:RenderTexture.active + ReadPixels + Apply

异步获取(推荐)

AsyncGPUReadback.Request(computeBuffer, (request) => {
    if (!request.hasError) {
        NativeArray<int> data = request.GetData<int>();
    }
});

注意:异步获取最快下一帧返回。

ComputeShader 常用 API

C# 侧

  • FindKernel:查找入口函数索引
  • Dispatch:启动计算
  • SetFloat/SetInt/SetBool/SetVector/SetMatrix/SetBuffer/SetTexture:设置参数
  • GetKernelThreadGroupSizes:获取 numthreads 配置
  • EnableKeyword/DisableKeyword:动态编译关键字
  • SystemInfo.supportsComputeShaders:判断设备支持

ComputeBuffer

  • new ComputeBuffer(count, stride):创建缓冲区
  • SetData:上传数据
  • GetData:同步读取
  • Release:释放资源
  • CopyCount:统计动态生成数据数量

HLSL 侧

  • 参数语义:SV_DispatchThreadID、SV_GroupThreadID、SV_GroupID
  • 内置函数:lerp、step、saturate、smoothstep、dot、cross、length、normalize 等
  • groupshared:组内共享内存
  • 同步函数:GroupMemoryBarrierWithGroupSync(最常用)
  • 原子操作:InterlockedAddInterlockedExchangeInterlockedCompareExchange

ComputeShader 应用场景

图形计算:后处理效果、光照计算、粒子系统、GPU 剔除、体积渲染、地形水面模拟、程序纹理生成、GPU 蒙皮

逻辑计算:并行排序、并行扫描、矩阵向量运算、图像处理、距离场计算

物理仿真 AI:流体模拟、布料模拟、刚体粒子、AI 路径搜索、神经网络加速

编辑器拓展:大规模数据离线预计算、纹理压缩转换、网格分析处理

项目典型用法

  • 移动端:GPU 模糊、Bloom、粒子模拟、屏幕空间特效
  • VR/AR:镜头畸变校正
  • 开放世界:GPU Culling + LOD + Instance Batch
  • 工业仿真:粒子、流体、温度场可视化

内存优化

移动端渲染性能优化

核心原则:移动设备上高 DrawCall 通常比填充率更容易造成性能瓶颈。

优化要点

优化方向 说明
避免 Alpha 测试 低端移动设备上透明度测试特别消耗资源,优先使用透明混合
最小化 DrawCall 利用批处理或手动合并网格降低 DrawCall,是移动平台提高性能的有效手段

资源优化:音频

音频加载方式优化

参数 作用 优化建议
Load Type 加载类型 背景音乐用 Streaming,短音效用 Decompress on Load
Preload Audio Data 预加载 常用音效勾选,大型音频取消勾选延迟加载
Compression Format 压缩格式 移动端背景音乐用 Vorbis,短音效用 ADPCM

生命周期管理:通过 AudioSource.Play() / Stop() 控制播放,不用的音频资源及时卸载。

资源优化:视频

视频加载方式优化

参数 选项 说明
Source URL 外部视频文件,streaming 方式加载
Source VideoClip 内部视频资源,可预加载
Render Mode Camera Far/Near Plane 渲染到摄像机,消耗 GPU
Render Mode Render Texture 渲染到纹理,可复用
Render Mode Material Override 替换材质纹理
Render Mode API Only 仅数据,不渲染,通过脚本访问

优化建议:使用 H.264 编码确保硬件解码;预分配 RenderTexture 避免运行时创建;播放完毕及时释放资源。

资源优化:纹理

纹理导入设置优化

参数 优化建议
Max Size 根据用途选择:UI 图标 128-256,角色纹理 1024-2048
Compression 移动端优先 ASTC,PC 用 BC7,根据平台选择
Mipmap UI 关闭,3D 物体开启
Read/Write 关闭可减少一半内存占用

运行时纹理管理:动态创建的 Texture2D 用完必须 Destroy();避免频繁调用 Texture2D.Apply()

资源优化:网格和动画

网格资源优化

参数 说明
Mesh Compression 压缩网格数据,Off/Low/Medium/High 权衡精度与体积
Read/Write 关闭可减少内存,但运行时无法读取顶点数据
Optimize Mesh 优化顶点顺序提升 GPU 缓存命中率

动画资源优化

参数 说明
Animation Compression Keyframe Reduction 减少关键帧数量
Curve Optimization 移除冗余曲线
Resample Curves 关闭可减少数据量,但可能影响精度

资源优化:材质和 Shader

材质优化要点

  • 公共材质使用 sharedMaterial,避免 material 属性隐式实例化
  • 动态创建的材质用完必须 Destroy()

Shader 变体控制

指令 特点
#pragma multi_compile 生成所有组合变体
#pragma shader_feature 只生成实际使用的变体

优化建议:控制变体数量,避免指数级增长;使用 shader_feature 减少不必要的变体。

资源优化:AB 包和 Resources

Resources 系统局限性

  • 启动时建立索引,影响启动速度
  • 无法动态更新,只能整包更新
  • 内存管理困难,UnloadUnusedAssets 开销大

AssetBundle 压缩方式对比

方式 压缩大小 加载速度 内存消耗 适用场景
LZMA 最小 最慢 最大 首次安装包、小体积下载
LZ4 中等 较快 上线项目推荐
Uncompressed 最大 最快 大量小文件、频繁加载

AB 包管理要点

  • 依赖关系:加载时需同时加载依赖包
  • 卸载策略:Unload(true) 卸载资源,Unload(false) 仅卸载 AB 包
  • 避免重复加载同一 AB 包

内存管理:基础优化技巧

垃圾回收时机选择

  • 避免频繁 GC,必要时在合适时机手动触发
  • 推荐时机:游戏暂停、过场景、读条界面
GC.Collect();
Resources.UnloadUnusedAssets();

结构体传递优化

  • 大结构体传递时考虑使用 ref 避免值拷贝开销

StringBuilder 替代 string

  • 多次循环拼接、大量 Replace、不确定长度的输入流 → 用 StringBuilder
  • 字符串常量、几次简单拼接 → 用 string

避免装箱拆箱

  • 使用泛型避免装箱
  • 注意 Debug.LogFormat("Frame {0}", Time.frameCount) 会装箱

数据内存布局

布局方式 特点 性能
class 数组 引用分散,缓存命中差 最差
struct 数组 数据连续,缓存友好 较好
数据数组(SoA) 每字段完全连续 最强

Unity API 数组问题

  • GetComponents<T>()Mesh.verticesrenderer.materials 等会分配新数组
  • 避免频繁调用,或缓存结果

内存管理:运行时高频操作优化

InstanceID 缓存

  • GetInstanceID() 有开销,初始化时获取一次并缓存

foreach 使用规则

  • 现代 Unity 版本可放心用于数组、List、Dictionary
  • 遍历 Unity 自身暴露的集合(如 Transform 子物体)需警惕,用 Profiler 确认

协程优化

  • 协程本质是状态机对象,启动有内存开销
  • 避免产生太多短生命周期协程
  • yield return new WaitForSeconds() 会产生堆内存分配

闭包问题

  • 闭包会捕获外部变量生成隐藏类
  • 可能延长对象生命周期、引发内存泄漏

Linq 和正则表达式

  • Linq:大量隐式堆分配、性能不透明、链式调用产生多个临时对象
  • 正则:运行成本极高(解释器 + 状态机)
  • 性能敏感逻辑(Update/FixedUpdate/AI/物理)不要使用

集合容量优化

  • new List<int>(预计数量) 提前指定容量避免扩容
  • 长期容器用 Clear() 重用,而非每次 new
  • Dictionary 同样建议初始化时指定容量

内存管理:资源管理与 GC 进阶

静态引用与事件退订

  • 静态字段、单例是根引用,持有对象不会被 GC
  • 事件订阅必须在 OnDisable/OnDestroy 中退订

资源释放方式

加载方式 释放方法
Resources.Load Resources.UnloadAsset(obj)UnloadUnusedAssets()
Addressables/AB 包 对应的 Release 或 Unload 方法
运行时创建资源 Destroy() 显式销毁

大数组与 LOH

  • 大于 85KB 的数组进入大对象堆(LOH),GC 成本更高
  • 优化:复用大数组、使用静态缓存、利用 ArrayPool<T>.Shared

非托管资源释放

资源类型 释放方法
NativeArray/NativeList .Dispose()
ComputeBuffer/RenderTexture .Release()Destroy()
GCHandle .Free()

增量 GC(Incremental GC)

  • 将单帧 GC 拆分成多帧执行,降低峰值卡顿
  • Player Settings → Other Settings → Configuration → Use incremental GC

DOTS 系统预告

  • 三大核心:ECS(实体组件系统)+ Job System(作业系统)+ Burst Compiler(编译器)
  • 适用场景:海量对象、大量重复计算、CPU 成为瓶颈时

115.3 面试题精选

基础题

1. Profiler 内嵌模式与独立进程模式有何区别?

题目

Profiler 的内嵌模式和独立进程模式有什么区别?分别在什么场景下使用?

深入解析
  • 内嵌模式:与 Unity Editor 同进程,上手快、调试方便;采样会占用同一进程的 CPU/内存,对结果有一定干扰,适合日常开发与局部分析。
  • 独立进程模式:Profiler 单独进程,与被测进程隔离,干扰更小,更适合大型场景、真机与接近发布环境的性能测试。
答题示例

内嵌:跟编辑器同进程,方便但采样有干扰。独立进程:隔离运行,干扰小,适合真机和正式向测试。

日常调试用内嵌;要数据更可信时用独立进程。

参考文章
  • 3.初识Profiler窗口

2. CPU Usage 里 Total 与 Self 各表示什么?如何用来找瓶颈?

题目

Profiler CPU Usage 模块中 Total 和 Self 两列分别表示什么?如何利用它们定位性能瓶颈?

深入解析
  • Total:该函数及其子调用链在本帧统计中的时间占比,反映整条路径的总开销。
  • Self:仅函数自身体的时间占比,不含子调用。
  • 用法:Total 高、Self 低 → 往子调用里钻;Self 高 → 本函数逻辑或 IO 可疑。结合 Calls 可看是否无意义高频调用。
答题示例

Total 是「自己+子函数」占比,Self 是「只算自己」占比。

Total 高 Self 低说明问题在子层;Self 高说明本函数要优化。配合 Calls 看是否重复调用过多。

参考文章
  • 5.Profiler窗口CPUUsage

3. 脚本里哪些「看起来无害」的写法会带来持续性能损耗?

题目

为什么不应保留空的生命周期函数(如空 Update)?发布版本如何降低 Debug.Log 一类日志的开销?

深入解析
  • 空生命周期:只要声明了 Update 等,Unity 仍会按帧调度,大量脚本时空调用会叠加成可见开销;不需要时应删掉方法本身。
  • 日志:可用二次封装 + 运行时常量开关,或用 [Conditional("UNITY_EDITOR")] / DEVELOPMENT_BUILD 让正式包不编译进调用路径,避免字符串格式化与 GC。
答题示例

空 Update 仍会被每帧调用,多了会积少成多,应直接删除声明。

发布版用条件编译或封装开关,避免 Debug.Log 在玩家包路径里执行。

参考文章
  • 25.生命周期函数
  • 26.Debug的性能消耗

进阶题

1. 开启 GPU Usage 为何会扭曲性能结论?URP/HDRP 下要注意什么?

题目

为什么开启 GPU Usage 会影响性能测试的准确性?在 URP/HDRP 项目中有什么限制?

深入解析
  • 采集 GPU 数据往往会 关闭 Graphics Jobs 等并行路径,CPU/GPU 侧行为与真实玩家环境不一致,测到的帧时间可能偏悲观或偏乐观。
  • SRP:GPU Usage 常无法拆到 Pass/Shader 粒度,多为汇总项;需要时用 Rendering Debugger、Xcode/Android GPU 工具等补充。
答题示例

GPU Profiler 会改调度与并行度,测出来不等于纯玩家环境。

URP/HDRP 往往看不到细粒度 GPU 时间,要换平台或 SRP 专用调试手段。

参考文章
  • 6.Profiler窗口GPUUsage

2. 如何从 Rendering 与合批角度判断 DrawCall 是否是瓶颈?批处理体系怎么选?

题目

Profiler Rendering 模块里应关注哪些指标?动态批处理、静态批处理、GPU Instancing、SRP Batcher 各适合什么情形?

深入解析
  • 指标:Batches、SetPass、Draw Calls 的关系;SetPass 高往往意味着状态切换多、合批难。
  • 静态批处理:适合 不移动 且可标记 Static 的物体;有额外内存代价。
  • 动态批处理:适合小网格、同材质等满足引擎限制的情形;顶点数等有上限。
  • GPU Instancing:大量 同网格同材质 实例。
  • SRP Batcher:URP/HDRP 上减 CPU 侧常量缓冲提交,不等价于减少 Draw Call 数量,Shader 需兼容且注意与 MPB 等限制。
答题示例

先看 Batches 和 SetPass,判断是 DC 多还是状态切换多。

静止场景倾向静态批;大量相同实例用 Instancing;URP/HDRP 默认用好 SRP Batcher;小动态物体再看动态批条件。

参考文章
  • 7.Profiler窗口Rendering
  • 45.动态批处理
  • 46.静态批处理
  • 47.GPU Instancing
  • 48.SRP Batcher
  • 49.DrawCall优化方案对比

3. Profiler Memory 与 Memory Profiler 包如何分工?驻留内存与已分配内存有何不同?

题目

Profiler 自带 Memory 模块与 Memory Profiler Package 在定位内存问题时如何配合?驻留内存(Resident)与已分配内存(Allocated)区别是什么?

深入解析
  • Profiler Memory:适合 实时 看走势、快速判断是否在涨、哪类分配在动。
  • Memory Profiler:适合 快照对比、引用链、泄漏路径;抓快照有成本。
  • 驻留 vs 已分配:驻留更接近 物理 RAM 中实际占用,过高易 OOM;已分配含预留与虚拟地址等,可反映趋势但未必立刻触顶物理上限。
答题示例

先用 Profiler Memory 看趋势,再用 Memory Profiler 抓前后快照对比、跟引用链。

驻留决定「会不会被系统杀」;已分配更多看「是不是在持续漏或预留失控」。

参考文章
  • 9.初识MemoryProfiler窗口
  • 11.MemoryProfiler窗口UnityObjects

深度题

1. 如何用 Profiler API 做自定义区间采样与运行时内存查询?

题目

如何在代码中使用 Profiler API 进行自定义性能采样?常用内存相关 API 有哪些?要注意什么?

深入解析
  • 命名空间 UnityEngine.ProfilingProfiler.BeginSample / EndSample 成对包裹热点;建议编辑器或宏包裹,避免打进正式包。
  • 内存:GetMonoUsedSizeLongGetTotalAllocatedMemoryLongGetTotalReservedMemoryLongGetRuntimeMemorySizeLong 等用于 趋势与对比,不能替代快照工具做引用分析。
  • 可开 enableBinaryLog 导出 .raw 离线打开;注意采样与日志本身也有开销。
答题示例

BeginSample/EndSample 标区间,成对使用,正式包用宏关掉。

GetTotalAllocatedMemoryLong 等看总量趋势;要查谁持有引用还得 Memory Profiler 快照。

参考文章
  • 22.Profiler窗口脚本控制

2. 用 Memory Profiler 做「泄漏」排查时,推荐的操作顺序是什么?

题目

如何使用 Memory Profiler 的快照对比定位疑似内存泄漏?典型根因有哪些?

深入解析
  1. 同一操作前后(开关 UI、切场景、跑一段时间)各抓一帧快照。
  2. Summary 看总量与分类涨幅;Unity Objects 按 Allocated Size 排序找异常增长类型。
  3. Compare 模式下列出的新增/增大对象;用 Referenced By 往上追到 静态字段、单例、未退订事件 等长生命周期根。
  4. All Of Memory 辅助看托管堆、Reserved、Untracked 等是否异常。
答题示例

前后各抓一张,先 Summary 看哪类涨,再在 Unity Objects 里排序、对比实例,顺着引用链找根。

常见根因:静态容器、事件没 -=、资源重复加载未卸载。

参考文章
  • 9.初识MemoryProfiler窗口
  • 10.MemoryProfiler窗口Summary
  • 11.MemoryProfiler窗口UnityObjects
  • 12.MemoryProfiler窗口AllOfMemory

3. 什么是本机-托管桥接?脚本侧如何系统性减少这类开销?

题目

本机-托管桥接为什么贵?结合 Transform、GetComponent、Update 等常见 API,如何优化?

深入解析
  • 桥接:托管 C# 调用引擎 C++ 的路径,涉及调度与封送,高频时每帧放大明显。
  • 典型触发GetComponenttransform 属性读写、SetActiveFind 等。
  • 手段:Awake/Start 缓存引用;减少 transform 逐属性读写(读入局部变量、批量更新);避免在 UpdateFind/反复 GetComponent;能用事件驱动就不用每帧轮询;必要时 集中 Update 管理器 降低调度边界次数。
答题示例

桥接就是 C# 调引擎原生层的跨界调用,次数多了 CPU 很明显。

组件和 Transform 在初始化缓存好;Update 里别 Find/GetComponent;逻辑能事件驱动就不每帧扫。

参考文章
  • 31.组件获取
  • 32.Update
  • 35.本机托管的桥接

4. ComputeShader 的执行模型是什么?numthreads 与 Dispatch 如何决定总线程数?

题目

ComputeShader 中线程、线程组、Dispatch 网格的关系是什么?SV_DispatchThreadID 与组内 ID 何时用?

深入解析
  • 层次:线程 → [numthreads(x,y,z)] 定义的 线程组Dispatch(gx,gy,gz) 定义的 组网格;总线程数为 (x*gx, y*gy, z*gz)(逐分量)。
  • numthreads:每组线程数,乘积受平台限制(如每线程组不超过 1024,各维有上限)。
  • 语义SV_DispatchThreadID 最常用于 一一映射 到像素/体素/数组下标;SV_GroupID + SV_GroupThreadID 适合 组内共享内存 与分块算法。关系:DispatchThreadID = GroupID * numthreads + GroupThreadID(按分量)。
答题示例

一组里有多少线程由 numthreads 定,有多少组由 Dispatch 定,乘起来是总并行度。

简单逐元素用 DispatchThreadID;要用 groupshared 或分块就用组 ID 加组内 ID。

参考文章
  • 96.ComputeShader的作用和基本结构
  • 100.ComputeShader组内线程数量
  • 101.ComputeShader三大参数语义

5. 托管堆上哪些习惯会放大 GC 压力?非托管与 Native 资源要如何收尾?

题目

装箱、Linq、大对象堆(LOH)、增量 GC、静态引用与事件订阅各带来什么问题?NativeArray、ComputeBuffer 等如何释放?

深入解析
  • 装箱与 Linq:热路径上会产生 隐式分配 与迭代器对象,引发更频的 GC;性能敏感处用 for 循环与泛型
  • LOH:大数组(如大于约 85KB)进 LOH,回收与碎片成本更高;用 复用、ArrayPool 等减少抖动。
  • 增量 GC:把停顿摊到多帧,换总体 CPU 略增;GC.Collect 仍要选时机(切场景、加载屏),避免滥用。
  • 泄漏感:静态字段、单例、事件未退订 会让对象达不到回收条件。
  • 非托管NativeArray / NativeListDispose()ComputeBuffer / RenderTextureRelease()Destroy();配合 usingtry/finally,避免异常路径泄漏。
答题示例

热路径少装箱、少 Linq;大缓冲用池化或 ArrayPool。

静态和事件长按引用要像管理资源一样在切场景或 OnDestroy 里断开;Native 侧必须 Dispose/Release,不能指望 GC。

参考文章
  • 112.内存优化-内存管理-基础内存优化技巧
  • 113.内存优化-内存管理-运行时高频操作内存优化
  • 114.内存优化-内存管理-资源管理与GC进阶优化


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

×

喜欢就点赞,疼爱就打赏