Unity内存/GC优化方法

【前言】

GC回收垃圾时会暂停所有线程,垃圾回收后再开启所有线程,如果垃圾回收的时间长,那么程序会出现明显的卡顿。这会导致游戏帧率下降,游戏在运行时可能卡卡顿顿,帧率下降。为了让整个程序运行过程始终保持顺畅,在进行对内存/GC进行优化,优化的方法分为三种:一是减少GC运行的时间;二是减少GC的运行次数;三是推迟GC运行时间。

【策略性的】

  • 对于子弹等不断重复出现消失的物体使用对象池Object Pooling管理
  • 游戏载入新场景时,显示调用GC.Collection回收垃圾(因为载入场景本身就要等待,GC运行造成卡顿也没事)
  • 使用SriptableObject(脚本化对象),其相当于资源文件,在实例化的时只是复制了引用(在实例化Prefab时是把数据复制了一遍)
  • 启用增量垃圾收集(增量垃圾收集将垃圾收集分解到若干帧中完成,避免出现尖峰,这些尖峰意味着进行垃圾收集那一帧的物理时间远远超过维持 60FPS 所需的 16 毫秒限制,这会导致游戏卡顿)
  • 不要在需要频繁调用的函数中写有会产生内存分配操作的代码,尤其是在Update中
  • 尽量为需要长时间存活的资源创建大对象。(>=85000字节的对象为大对象)
  • 减少装箱拆箱代码
  • 字符串内存优化

【代码相关的】

  • 如果某个方法只需要在某段时间内或某个条件下重复调用,而不是一直被重复调用,使用Invoke Repeating或者Coroutine来替代Update方法。(Update会被每帧调用,即使里面只有一行代码,也会耗时的,用协程虽然也会产生内存,但好处在于之后就不会再被调用。)
  • Foreach内部有迭代器,会导致装箱操作,所以在可以确定数组大小的情况下,用for循环代替。数组可以List就不可以这样用
  • Debug.log()会消耗很多性能,游戏完成后尽量去掉无用的Debug
  • 减少Find,Component的使用,可以在Init中直接一次获取,而不是用的时候再获取。
  • 慎用单例并管理好单例,因为静态中的引用不会被释放,如果其引用的某个东西引用了一个资源,那么资源不会被释放,内存一直占着。
  • 注意协程造成的GC,协程中的yield语句本身不需要进行堆内存分配, 但由yield return带来的返回值可能需要分配堆内存,调用StartCoroutine()方法会产生少量的垃圾, 因为Unity需要创建一些类的实例用于管理协程。注意,不要让某一个协程长时间存在,因为协程只要没被释放,里面的所有变量,即使是局部变量(包括值类型),也都会在内存里
  • 使用缓存在实现变量的重复利用
void OnTriggerEnter(Collider other)
{
     Renderer[] allRenderers = FindObjectsOfType<Renderer>();//反复执行该方法时,每次都需要重新给数组分配内存
     ExampleFunction(allRenderers);       
}


//改进
private Renderer[] allRenderers;//定义一个类的成员变量,只会产生一个数组用来缓存,实现重复利用以减少不必须要的内存分配

void Start()
{
   allRenderers = FindObjectsOfType<Renderer>();
}

void OnTriggerEnter(Collider other)
{
    ExampleFunction(allRenderers);
}

  • 合理使用数据结构与算法,例如了解常用的List<T>,Array,Stack<T>,Queue<T>,Dictionary<TKey,TValue>的源码以便更合理地使用
  • 从脚本中获取材质时使用Renderer.sharedMaterial而不是Renderer.Material,后者会产生一份新的copy
  • struct中最好不要有引用类型的变量,这会使得GC会检测整个struct
  • 如果这个类被高频应用,且类中都是对字段的操作,没有引用类型,改用struct
  • 一个脚本写完后,可以看看哪些临时变量可以去除
  • 将暂时不用的以后还需要使用的物体隐藏起来而不是直接销毁
  • 大量字符串拼接的操作用StringBuilder
  • 避免频繁Instantiate来实例化GameObject,这会导致很多内存分配
  • 释放AssetBundle占用的资源
  • 尽量避免使用静态字段(静态字段所引用的对象不会被GC回收,如果该对象较大或该对象又引用了很多其他对象,那么这一系列的对象都一直存活。如果在接下来的程序中,我们不需要这些对象了,那么一直存活的这些对象会占用内存。)
  • for循环嵌套时把循环次数多的放在里面for 循环嵌套性能的比较_lengxiao_wang的博客-CSDN博客
  • 利用缓存,将需要重复调用的方法中的临时变量改为类的成员变量,例如Update要调用很多次,则需要产生很多临时变量的内存垃圾。
  • 链表重用与清除,同变量缓存类似,对链表、字典等需要反复用到的在用完时清除
  • 对具有返回值的Unity函数和第三方插件函数,返回结果时需要分配内存,同样采用变量缓存的方式定义一个变量来接收结果
void ExampleFunction()
{
    for(int i=0; i < myMesh.normals.Length;i++)
    {
        Vector3 normal = myMesh.normals[i];
    }
}

//改进
void ExampleFunction()
{
    Vector3[] meshNormals = myMesh.normals;
    for(int i=0; i < meshNormals.Length;i++)
    {
        Vector3 normal = meshNormals[i];
    }
}

  • 对GameObject.name和GameObject.tag的获取会分配内存在存储返回的字符串,如果需要比较判断时,用GameObject.CompareTag()来替代
     
  • 对于需要在Update中但不需要每帧都运行的函数,设置定时器来保证每隔一段时间触发该函数
private float timeSinceLastCalled;
private float delay = 1f;
void Update()
{
    timSinceLastCalled += Time.deltaTime;
    if(timeSinceLastCalled > delay)
    {
         ExampleGarbageGenerationFunction();
         timeSinceLastCalled = 0f;
    }
}
  • 不要在Update中频繁更新Text的内容,因为每次调用Update时都会分配新的字符串,源源不断产生垃圾,应该先检测Text的内容是否会发生变化。代码如下。
//每帧要为scoreText分配空间,仅用一次后就无用了
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public Text scoreBoard;
    public int score;
    
    void Update() {
        string scoreText = "Score: " + score.ToString();
        scoreBoard.text = scoreText;
    }
}

//改进
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public Text scoreBoard;
    public string scoreText;
    public int score;
    public int oldScore;
    
    void Update() {
        if (score != oldScore) {
            scoreText = "Score: " + score.ToString();
            scoreBoard.text = scoreText;
            oldScore = score;
        }
    }
}
  • 如果要销毁一个物体,不要置为Null,而是要Destroy,然后置为Null。(因为置为Null,在C++层未销毁)

  • 减少匿名函数和闭包的使用(所有的匿名函数和闭包在c#编IL代码时都会被new成一个Class(匿名class),所以在里面所有函数,变量以及new的东西,都是要占内存的)

【资源相关的】

在复杂的大中型项目中,各种资源所占用的内存往往会达到游戏运行所占用的总体内存的70%以上。因此,资源可以直接决定项目内存占用情况,优化资源可以达到优化内存占用的目的。一般来说,游戏项目的资源主要分为以下几种:纹理 ( Texture ) , 网格 ( Mesh ) , 动画片段 ( AnimationClip ) , 音频片段 ( AudioClip ) , 材质 ( Material ) , 着色器 ( Shader ) , 字体资源 ( Font ) 以及文本资源 ( Text Asset ) 等等。其中,纹理、网格、动画片段、音频片段更容易占用更多的内存,即游戏运行时造成更多的内存开销。

  • 对纹理的优化
    • 纹理格式:RGBA32和RGBA16格式的纹理资源, 较之"硬件支持"格式, 会在加载时占据更大的CPU 耗时。ETC1 格式纹理, ETC1不带透明通道, 需要准备两个纹理 ( RGBETC1纹理+AlphaETC1纹理 )ETC2 格式纹理. 对于Android 平台,该格式仅能在OpenGLES3.0 的设备上被硬件支持。PVRTC 格式纹理. 支持iOS低端手机, 纹理尺寸必须是二次方正方形。Unity是支持软解的, 在不支持的设备上是可以运行游戏的. 软解慢又占内存。
    • 纹理尺寸:长宽分辨率为2的整数幂,便于压缩。
    • Read & Write:开启该选项将会使纹理内存增大一倍. 这个纹理既在显存上, 又在内存上, 而且还必须得是非压缩的格式。
  • 对网格的优化
    • 网格资源中含有Color 数据, Normal 数据和Tangent 数据,  这些数据的存在会增大网格资源文件大小和内存占用。Color 数据和Normal数据主要为3DMax,  Maya 等建模软件导出时设置所生成,  而Tangent一般为导入引擎时生成。一般来说,  这些数据主要为Shader 所用,  来生成较为酷炫的效果。尝试开启"Optimize Mesh Data"选项, 勾选后, 引擎在发布时会遍历所有的网格数据, 将多余数据去除, 从而降低Mesh文件大小。这里的“多余数据”是指渲染时Shader 不需要的数据。
  • 配置表
    • 如果配置表太大了,不要一次性加载到内存中,分关、分时机加载。

【Unity内存检测工具】

  • UnityProfiler、MemoryProfiler
  • 第三方工具:WeTest/UWA/Testplus

未完待续。。。

【参考】

Unity优化之GC——合理优化Unity的GC - zblade - 博客园

了解自动内存管理 - Unity 手册

https://www.yuque.com/jingangxin36/unity/gnx6ec

Unity优化之GC——合理优化Unity的GC_慕课手记

  • 10
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity中实现内存优化可以提高游戏的性能和稳定性。以下是一些常见的内存优化技巧: 1. 使用对象池:对象池是一种重复使用对象的技术,可以避免频繁地创建和销毁对象,从而减少内存分配和垃圾回收的开销。 2. 减少资源加载:尽量避免在运行时频繁加载资源,可以通过预加载、异步加载、动态加载等方式优化资源管理。 3. 优化纹理使用:使用合适的压缩格式和分辨率来减小纹理的内存占用,避免同时加载过多的高分辨率纹理。 4. 减少不必要的引用:及时释放不再使用的对象和资源,并确保没有循环引用导致无法释放的内存泄漏。 5. 使用内存优化工具:Unity提供了一些内存优化工具和分析器,如Profiler和Memory Profiler,可以帮助你检测和解决内存泄漏和性能问题。 6. 避免频繁的实例化和销毁:尽量重用对象,避免频繁地实例化和销毁大量对象,可以使用对象池或者对象复用技术。 7. 限制使用动态内存分配:尽量避免频繁地使用动态内存分配函数(如new和malloc),可以使用对象池或者预分配内存的方式来减少动态内存分配的次数。 8. 优化代码逻辑:优化算法和代码逻辑,减少不必要的计算和内存消耗。 这些只是一些常见的内存优化技巧,具体的优化策略还需要根据具体的项目需求和情况来进行调整和实施。同时,使用性能分析工具和测试工具来评估和验证优化效果也是很重要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值