会频繁更新 主要给自己看
性能优化总结:成本和分析
工具:Memory Profiler/Unity Editor Profiler/Frame Debugger/RenderDoc/ VS / ADB /Instruments / GPA /Inter VTune Profiler /Unity Bulid Report Tool
优化大纲:
1:CPU
(1)渲染模块
简化资源:
- Back Face Culling(把看不到的三角面剔除)
- Facing Culling(把看不到的区域剔除)
- Cull Distance Volume(基于距离的Culling)
- Landscape Culling Settings(地形剪裁Culling)
- LOD & HLOD (数据剪裁Culling)
- Streaming System(数据占用Culling)
- Precomputed Visibility Volume (离线Culling缓解实时Culling的压力)
降低DrawCall:
- 静/动批处理
- 图集:把UI的图片打成图集 减少DrawCall 但相应的可能会导致资源都大 加载的时候可能会加载多余的资源到内存但又不使用 解决办法就是根据功能去分类打图集 并且图集要进行合理安排 像背景这种大图片就不需要打进去了 尽量使得合图尽量减少空白 并且保持合图不会特别大
- GPU Instance技术
动态批处理要求
- 顶点属性的最大限制为900,而且未来有可能会变。不要依赖这个数据。
- 一般来说,那么所有对象都必须需要使用同一个缩放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必须都一样)。但如果是非统一缩放(即每个维度的缩放尺度不一样,例如(1, 2, 1)),那么如果所有的物体都使用不同的非统一缩放也是可以批处理的。这个要求很怪异,为什么批处理会和缩放有关呢?这和Unity背后的技术有关系,有兴趣的可以自行谷歌,比如这里。
- 使用lightmap的物体不会批处理。多passes的shader会中断批处理。接受实时阴影的物体也不会批处理。
合批常遇问题解决:
- Mask影响合批 如果遮罩区域只是矩形的话直接替换成Rect2DMask 如果还是想用不规则图片做遮罩的话直接重写mask
(2)UI模块
- 不需要Graphic Raycaster的关闭掉
- 动静分离
- 提高填充率:(1)把透明图片按钮用其他方式替换掉 (2)把图片的空白透明的地方干掉
- 全屏界面时关闭3D渲染(把3D场景的摄像机关闭)和画布
- 停止在界面外的UI渲染
- 变化的UI会影响合批,建议:根据更新频率把不同的UI分布到不同的画布上 Z轴保持一致 相同材质和使用矩形裁剪(mask裁剪会影响合批 且因为其是模板测试的方式裁剪导致像圆形头像这种裁剪完会有锯齿)
(3):加载模块
根据不同的资源特点使用不同的加载方式 比如:缓存 流式加载 异步加载 多线程等
- Resources会影响启动速度 因为文件的标题会变成所有资源的索引表 这个索引表会在游戏开始后载入内存 以更有效的方式进行资源管理和卸载 比如热更新常用的assetbundle资源管理
(4):代码效率
对耗时的代码进行优化
- 更改参数或者替换算法来降低算法复杂度 比如:排序算法 查找算法等
- 给外部材质传递参数的时候我们往往会直接根据字符串名直接传递 但其内部其实是ID 频繁调用会导致多次转换 所以我们会需要先把字符串转成ID并且缓存下来在使用
- GetComponent会遍历对象上面的所有组件去寻找 组件越多GetComponent的成本就越高 Find会寻找遍历当前场景所有对象 对象越多成本越高 建议使用缓存的方式优
- 通过坐标转换来移动有刚体组件的对象会导致PhysX物理引擎整体重新计算 建议用rigidBody自带的方法并且放到FixedUpdate去
- AddComponent会有一系列的检查过程 为了减少这些过程 建议不要在运行时在去AddComponent一些组件 而是预先制作好预设体
- 移除空的Unity事件函数 因为其依然在内部经行一系列操作
- Awake和Start完成后才会开始游戏 初始化内容太多会导致启动速度变慢 建议启动游戏时初始化内容较少 进入游戏在开始各种初始化过程
- 层级太深会导致父物体变化,所有子物体跟着变化,而且垃圾回收也需要遍历很多次才能拿到所有对象
2:内存优化
资源优化:
GC优化(值类型的内存是由stack分配的 引用类型的内存是heap分配的)
(1)定期调用GC:比如场景切换的时候就进行一次GC操作
(2)stack(堆栈) 包含:函数本身,函数传的参数,函数区域的变量 用完就从堆栈里面丢出去 太多会造成堆栈溢出
- 在编辑器时候才使用get/set(因为get/set属于方法调用 花费在堆栈框中的时间会增加) 真机上就正常获取数据
(3)heap(堆) 包含:类,实体,对象 用完只会标记为未使用 需要使用垃圾回收器去回收
- 缓存:对于不会变化的对象预先缓存下来,后面可以重复使用 比如使用Scriptable Objects减少我们自己写的类的里面的变量数据在new的时候多次生成
- 对象池:预先实例化n个对象 循环使用这几个对象 多了就扩容
(4)以下这些操作会造成内存分配 用的时候要尽量避免或者用其他方法代替
- 干掉非必要的Log日志打印
- 字符串拼接,unity部分API(太多,可自查,这里不放了),装箱操作,协程,foreach(unity5.5之前),函数引用,LINQ和常量表达式
- struct的结构如果有值类型和引用类型的话 需要分开他们
public struct ItemData
{
public string name;
public int cost;
public Vector3 position;
}
private ItemData[] itemData;
我们可以将该struct拆分为多个数组的形式,从而减小GC的工作量:
private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;
- 遇到对象引用其他对象的可以使用标记来代替引用
public class DialogData
{
private DialogData nextDialog;
public DialogData GetNextDialog()
{
return nextDialog;
}
}
通过重构代码,我们可以返回下一个对话框实体的标记,而不是对话框实体本身,这样就没有多余的object引用,从而减少GC的工作量:
public class DialogData
{
private int nextDialogID;
public int GetNextDialogID()
{
return nextDialogID;
}
}
引擎模块自身占用:
对于WebStream和SerializedFile,你需要关注以下两点:
- 是否存在AssetBundle没有被清理干净的情况。开发团队可以通过Unity Profiler直接查看其使用具体的使用情况,并确定Take Sample时AssetBundle的存在是否合理;
- 对于占用WebStream较大的AssetBundle文件(如UI Atlas相关的AssetBundle文件等),建议使用LoadFromCacheOrDownLoad或CreateFromFile来进行替换,即将解压后的AssetBundle数据存储于本地Cache中进行使用。这种做法非常适合于内存特别吃紧的项目,即通过本地的磁盘空间来换取内存空间。
无效的Mono堆内存开销
避免或减少过多“无效堆内存":
- 避免一次性堆内存的过大分配。Mono的堆内存也是“按需”逐步进行分配的。但如果一次性开辟过大堆内存,比如New一个较大Container、加载一个过大配置文件等,则势必会造成Mono的堆内存直接冲高,所以研发团队对堆内存的分配需要时刻注意;
- 避免不必要的堆内存开销。
资源冗余:干掉多余重复的资源
- 变体优化:表面着色器改成vert/frag 去掉不需要的变体和Pass
- 去掉多余重复的资源
内存泄露查询方法:
- 检查资源的使用情况,特别是纹理、网格等资源的使用
- 通过Profiler来检测WebStream或SerializedFile的使用情况
- 通过Android PSS/iOS Instrument反馈的App线程内存来查看
4:渲染优化
- 方案替换(阴影 抗锯齿 后处理等实现方法多种多样,可以挑选一个最适合项目的)
- 加速结构(运用数据结构的特性加速运算,比如:层次包围盒)
- 加速算法(运用数学的特性加速运算,比如:重要性采样)
- 算法优化:减少ifelse,clip使用 避免除0 符号替换(比如:除法换乘法 乘法换加法) 使用RTT代替GrabPass等
- OverDraw:相机和场景分开 不透明物体从后往前 Early Depth testing(提前深度测试) 增加填充率
5:资源制作过程优化
- 移除不需要透明度的纹理的alpha通道
- 合并mesh/贴图减少内存和构建时间
- 底模仿高模
6:编译速度优化
- 代码固定的可制作成DLL
7:其他
- 自定义分辨率而不是使用手机自带的(因为现在的手机分辨率越来越大,但我们其实并不需要那么大)
- 数据结构选择