Unity性能优化4【内存实战篇】

本文介绍Unity内存优化的具体方案

1.缓存函数返回值

如果我们的代码重复调用导致堆分配的函数,然后丢弃结果,则会产生不必要的垃圾。相反,我们应该存储对这些对象的引用并重用它们。这种技术称为缓存。

在以下示例中,代码每次调用时都会导致堆分配。这是因为创建了一个新数组。

void OnTriggerEnter(Collider other)
{Renderer[] allRenderers = FindObjectsOfType<Renderer>();ExampleFunction(allRenderers);
}

以下代码仅导致一次堆分配,因为数组被创建并填充一次,然后被缓存。缓存的数组可以一次又一次地重复使用,而不会产生更多的垃圾。

private Renderer[] allRenderers;void Start()
{allRenderers = FindObjectsOfType<Renderer>();
}void OnTriggerEnter(Collider other)
{ExampleFunction(allRenderers);
}

2.不要在频繁调用的函数中分配堆内存

如果我们必须在 MonoBehaviour 中分配堆内存,最糟糕的地方就是在频繁运行的函数中。例如,Update() 和 LateUpdate() 每帧调用一次,因此如果我们的代码在这里生成垃圾,它会很快增加。我们应该考虑在可能的情况下缓存对 Start() 或 Awake() 中对象的引用,或者确保导致分配的代码仅在需要时运行。

让我们看一个非常简单的示例,移动代码使其仅在情况发生变化时运行。在下面的代码中,每次调用 Update() 时都会调用一个导致分配的函数,从而频繁地产生垃圾:

void Update()
{ExampleGarbageGeneratingFunction(transform.position.x);
}

通过简单的更改,我们现在确保仅当transform.position.x 的值发生更改时才调用分配函数。我们现在只在必要时进行堆分配,而不是在每个帧中进行分配。

private float previousTransformPositionX;void Update()
{float transformPositionX = transform.position.x;if (transformPositionX != previousTransformPositionX){ExampleGarbageGeneratingFunction(transformPositionX);previousTransformPositionX = transformPositionX;}
}

另一种减少 Update() 中生成的垃圾的技术是使用计时器。这适用于当我们有一段生成垃圾的代码必须定期运行但不一定每帧运行时。在以下示例代码中,生成垃圾的函数每帧运行一次:

void Update()
{ExampleGarbageGeneratingFunction();
}

在下面的代码中,我们使用计时器来确保生成垃圾的函数每秒运行一次。

private float timeSinceLastCalled;private float delay = 1f;void Update()
{timeSinceLastCalled += Time.deltaTime;if (timeSinceLastCalled > delay){ExampleGarbageGeneratingFunction();timeSinceLastCalled = 0f;}
}

当对频繁运行的代码进行这样的小更改时,可以大大减少生成的垃圾量。

3.用清理集合代替频繁创建集合

创建新集合会导致在堆上进行分配。如果我们发现在代码中多次创建新集合,我们应该缓存对集合的引用并使用 Clear() 清空其内容,而不是重复调用 new

在以下示例中,每次使用 new 时都会发生新的堆分配。

void Update()
{List myList = new List();PopulateList(myList);
}

在以下示例中,仅当创建集合或必须在后台调整集合大小时才会发生分配。这大大减少了垃圾的产生量。

private List myList = new List();void Update()
{myList.Clear();PopulateList(myList);
}

4.使用对象池代替频繁创建销毁对象

即使我们减少脚本中的分配,如果我们在运行时创建和销毁大量对象,我们仍然可能会遇到垃圾收集问题。对象池是一种可以通过重用对象而不是重复创建和销毁对象来减少分配和释放的技术。对象池在游戏中应用广泛,最适合我们频繁生成和销毁相似对象的情况;例如,用枪射击子弹时。

5.合理使用String

在 C# 中,字符串是引用类型,尽管它们似乎保存着字符串的“值”。这意味着创建和丢弃字符串会产生垃圾。由于字符串在很多代码中常用,因此这些垃圾确实会增加。

C# 中的字符串也是不可变的,这意味着它们的值在首次创建后就无法更改。每次我们操作一个字符串(例如,使用 + 运算符连接两个字符串)时,Unity 都会使用更新的值创建一个新字符串并丢弃旧字符串。这会产生垃圾。

我们可以遵循一些简单的规则来将字符串中的垃圾降至最低。让我们考虑一下这些规则,然后看一个如何应用它们的示例。

1. 我们应该减少不必要的字符串创建。如果我们多次使用相同的字符串值,我们应该创建一次字符串并缓存该值。

2. 我们应该减少不必要的字符串操作。例如,如果我们有一个经常更新并包含连接字符串的文本组件,我们可以考虑将其分成两个文本组件。

3. 如果我们必须在运行时构建字符串,我们应该使用 StringBuilder 类。 StringBuilder 类旨在构建无需分配的字符串,并且可以减少连接复杂字符串时产生的垃圾量。

4. 一旦调试不再需要对 Debug.Log() 的调用,我们就应该删除它们。对 Debug.Log() 的调用仍然在我们游戏的所有版本中执行,即使它们不输出任何内容。对 Debug.Log() 的调用会创建并处理至少一个字符串,因此如果我们的游戏包含许多此类调用,则垃圾会增加。

让我们看一下一个代码示例,该代码通过低效使用字符串生成不必要的垃圾。在下面的代码中,我们通过将字符串“TIME:”与浮点计时器的值组合起来,在 Update() 中创建一个用于显示分数的字符串。这会产生不必要的垃圾。

public Text timerText;
private float timer;void Update()
{timer += Time.deltaTime;timerText.text = "TIME:" + timer.ToString();
}

在下面的示例中,我们已经大大改进了事情。我们将单词“TIME:”放在单独的 Text 组件中,并在 Start() 中设置其值。这意味着在 Update() 中,我们不再需要组合字符串。这大大减少了垃圾的产生量。

public Text timerHeaderText;
public Text timerValueText;
private float timer;void Start()
{timerHeaderText.text = "TIME:";
}void Update()
{timerValueText.text = timer.toString();
}

6.使用占用内存较少的API

重要的是要意识到,每当我们调用不是我们自己编写的代码时,无论是在 Unity 本身还是在插件中,我们都可能会生成垃圾。一些 Unity 函数调用会创建堆分配,因此应小心使用,以避免生成不必要的垃圾。

没有我们应该避免的函数列表。每个功能在某些情况下可能很有用,而在其他情况下则不太有用。与以往一样,最好仔细分析我们的游戏,确定垃圾产生的位置,并仔细考虑如何处理它。在某些情况下,缓存函数的结果可能是明智的;在其他情况下,减少调用该函数的频率可能是明智的;在其他情况下,最好重构我们的代码以使用不同的函数。话虽如此,让我们看一下导致堆分配的 Unity 函数的几个常见示例,并考虑如何最好地处理它们。

每次我们访问返回数组的 Unity 函数时,都会创建一个新数组并将其作为返回值传递给我们。此行为并不总是显而易见或符合预期,尤其是当函数是访问器时(例如,Mesh.normals)。

在以下代码中,为循环的每次迭代创建一个新数组。

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

在这种情况下减少分配很容易:我们可以简单地缓存对数组的引用。当我们这样做时,只会创建一个数组,并且创建的垃圾量也会相应减少。

下面的代码演示了这一点。在这种情况下,我们在循环运行之前调用 Mesh.normals 并缓存引用,以便只创建一个数组。

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

堆分配的另一个意外原因可以在函数 GameObject.name 或 GameObject.tag 中找到。这两个都是返回新字符串的访问器,这意味着调用这些函数将产生垃圾。缓存该值可能很有用,但在这种情况下,我们可以使用一个相关的 Unity 函数来代替。要根据值检查 GameObject 的标签而不生成垃圾,我们可以使用 GameObject.CompareTag()。

在以下示例代码中,垃圾是通过调用 GameObject.tag 创建的:

private string playerTag = "Player";void OnTriggerEnter(Collider other)
{bool isPlayer = other.gameObject.tag == playerTag;
}
If we use GameObject.CompareTag(), this function no longer generates any garbage:
private string playerTag = "Player";void OnTriggerEnter(Collider other)
{bool isPlayer = other.gameObject.CompareTag(playerTag);
}

GameObject.CompareTag 不是唯一的;许多 Unity 函数调用都有不会导致堆分配的替代版本。例如,我们可以使用Input.GetTouch() 和Input.touchCount 代替Input.touches,或者使用Physics.SphereCastNonAlloc() 代替Physics.SphereCastAll()。

7.避免装箱

装箱是指使用值类型变量代替引用类型变量时发生的情况的术语。当我们将值类型变量(例如 int 或 float)传递给具有对象参数(例如 Object.Equals())的函数时,通常会发生装箱。

例如,函数 String.Format() 采用一个字符串和一个对象参数。当我们向它传递一个字符串和一个 int 时,必须对 int 进行装箱。因此,以下代码包含装箱示例:

void ExampleFunction()
{int cost = 5;string displayString = String.Format("Price: {0} gold", cost);
}

装箱之所以会产生垃圾,是因为幕后发生的事情。当值类型变量被装箱时,Unity 在堆上创建一个临时 System.Object 来包装该值类型变量。 System.Object 是一个引用类型变量,因此当这个临时对象被释放时,会产生垃圾。

装箱是不必要的堆分配的一个极其常见的原因。即使我们没有直接在代码中对变量进行装箱,我们也可能使用导致装箱的插件,或者它可能发生在其他函数的幕后。最佳实践是尽可能避免装箱并删除任何导致装箱的函数调用。

8.减少携程产生的内存分配

调用 StartCoroutine() 会产生少量垃圾,因为 Unity 必须创建类实例来管理协程。考虑到这一点,当我们的游戏是交互式的并且性能是一个问题时,对 StartCoroutine() 的调用应该受到限制。为了减少以这种方式产生的垃圾,任何必须在性能关键时刻运行的协程都应该提前启动,并且在使用可能包含对 StartCoroutine() 的延迟调用的嵌套协程时我们应该特别小心

协程中的yield语句本身不会创建堆分配;然而,我们通过yield语句传递的值可能会产生不必要的堆分配。例如,以下代码会产生垃圾:

yield return 0;

此代码会产生垃圾,因为值为 0 的 int 被装箱。在这种情况下,如果我们希望简单地等待帧而不引起任何堆分配,最好的方法是使用以下代码:

yield return null;

协程的另一个常见错误是在多次产生相同值时使用 new。例如,以下代码将在每次循环迭代时创建并释放一个 WaitForSeconds 对象:

while (!isComplete)
{yield return new WaitForSeconds(1f);
}

如果我们缓存并重用 WaitForSeconds 对象,那么产生的垃圾就会少得多。以下代码以此为例:

WaitForSeconds delay = new WaitForSeconds(1f);while (!isComplete)
{yield return delay;
}

如果我们的代码由于协程而产生大量垃圾,我们可能希望考虑重构我们的代码以使用协程以外的东西。重构代码是一个复杂的主题,每个项目都是独一无二的,但我们可能希望记住一些协程的常见替代方案。例如,如果我们主要使用协程来管理时间,我们可能希望简单地在 Update() 函数中跟踪时间。如果我们主要使用协程来控制游戏中事件发生的顺序,那么我们可能希望创建某种消息传递系统来允许对象进行通信。没有一种万能的方法可以实现这一点,但记住,在代码中通常有不止一种方法可以实现相同的目标,这是很有用的。

9.减少垃圾收集器的校验工作

我们的代码的结构方式会影响垃圾收集。即使我们的代码不创建堆分配,它也会增加垃圾收集器的工作负载

我们的代码不必要地增加垃圾收集器工作量的一种方法是要求它检查不应该检查的东西。结构体是值类型变量,但如果我们有一个包含引用类型变量的结构体,那么垃圾收集器必须检查整个结构体。如果我们有大量这些结构,那么这可能会给垃圾收集器带来很多额外的工作。

在此示例中,结构体包含一个引用类型的字符串。现在,垃圾收集器在运行时必须检查整个结构数组。

public struct ItemData
{public string name;public int cost;public Vector3 position;
}
private ItemData[] itemData;

在此示例中,我们将数据存储在单独的数组中。当垃圾收集器运行时,它只需要检查字符串数组,可以忽略其他数组。这减少了垃圾收集器必须完成的工作。

private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

10.减少对象引用

我们的代码不必要地增加垃圾收集器工作负载的另一种方式是拥有不必要的对象引用。当垃圾收集器搜索堆上对象的引用时,它必须检查代码中的每个当前对象引用。即使我们不减少堆上的对象总数,代码中的对象引用越少意味着要做的工作就越少。

在此示例中,我们有一个填充对话框的类。当用户查看该对话框时,将显示另一个对话框。我们的代码包含对应显示的 DialogData 的下一个实例的引用,这意味着垃圾收集器必须检查此引用作为其操作的一部分:

public class DialogData
{private DialogData nextDialog;public DialogData GetNextDialog(){return nextDialog;}
}

在这里,我们重构了代码,以便它返回一个用于查找 DialogData 的下一个实例的标识符,而不是实例本身。这不是对象引用,因此不会增加垃圾收集器所花费的时间。

public class DialogData
{private int nextDialogID;public int GetNextDialogID(){return nextDialogID;}
}

就其本身而言,这个例子相当简单。然而,如果我们的游戏包含大量持有其他对象引用的对象,我们可以通过以这种方式重构代码来大大降低堆的复杂性。

11.合适的时间强制垃圾回收

最后,我们可能希望自己触发垃圾收集。如果我们知道堆内存已分配但不再使用(例如,如果我们的代码在加载资源时生成了垃圾)并且我们知道垃圾收集冻结不会影响播放器(例如,在加载屏幕时)仍然显示),我们可以使用以下代码请求垃圾收集:

System.GC.Collect();

这将强制垃圾收集器运行,在我们方便的时候释放未使用的内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/460593.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3.2 大数据概念、特征与价值

文章目录 大数据的概念美国高德纳咨询公司的定义麦肯锡全球研究所的定义狭义和广义的大数据 大数据的特征Volume&#xff08;体积&#xff09;Variety&#xff08;种类&#xff09;Velocity&#xff08;速度&#xff09;Value&#xff08;价值&#xff09;Veracity&#xff08;…

扫雷游戏(C语言详解)

扫雷游戏&#xff08;C语言详解&#xff09; 放在最前面的1、前言&#xff08;扫雷游戏的简介&#xff09;2、扫雷游戏的规则&#xff08;简易版&#xff09;3、代码实现&#xff08;3.1&#xff09;提醒一下&#xff1a;( i ) 提醒1&#xff1a;( ii ) 提醒2&#xff1a; &…

WPF+MVVM案例实战(十四)- 封装一个自定义消息弹窗控件(下)

文章目录 1、案例效果2、弹窗空间使用1.引入用户控件2、按钮命令实现 3、总结4、源代码获取 1、案例效果 2、弹窗空间使用 1.引入用户控件 打开 Wpf_Examples 项目&#xff0c;在引用中添加用户控件库&#xff0c;在 MainWindow.xaml 界面引用控件库&#xff0c;代码如下&…

银河麒麟v10 xrdp安装

为了解决科技被卡脖子的问题&#xff0c;国家正在大力推进软硬件系统的信创替代&#xff0c;对于一些平时对Linux操作系统不太熟练的用户来讲提出了更高的挑战和要求。本文以银河麒麟v10 24.03为例带领大家配置kylin v10的远程桌面。 最近公司为了配置信创开发新购了几台银河麒…

Python小游戏17——飞机大战

运行结果 首先&#xff0c;你需要安装Pygame库。如果你还没有安装它&#xff0c;可以使用以下命令来安装&#xff1a; bash pip install pygame 代码&#xff1a; python import pygame import random # 初始化Pygame pygame.init() # 屏幕大小 SCREEN_WIDTH 800 SCREEN_HEIGH…

国标GB28181软件EasyGBS国标GB28181网页直播平台在邮政快递场景中的应用

随着电子商务的迅猛发展&#xff0c;邮政快递行业迎来了前所未有的发展机遇&#xff0c;但同时也面临着诸多挑战。如何在保障货物安全、提高运输效率的同时&#xff0c;实现全面的监控和管理&#xff0c;成为邮政快递企业亟需解决的问题。国标GB28181网页直播平台EasyGBS作为一…

MFC工控项目实例二十七添加产品参数

承接专栏《MFC工控项目实例二十六创建数据库》 在型号参数界面添加三个参数试验时间、最小值、最大值。变量为double m_edit_time; double m_edit_min; double m_edit_max; 1、在SEAL_PRESSURE.h中添加代码 class CProductPara { public:union{struct{...double m_edit_min;…

java项目之智能学习平台系统源码(springboot)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的智能学习平台系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风…

Efficient Cascaded Multiscale Adaptive Network for Image Restoration 论文阅读笔记

Efficient Cascaded Multiscale Adaptive Network for Image Restoration 论文阅读笔记 这是新国立和新加坡管理大学发表在ECCV2024上的一篇image restoration的文章&#xff0c;提出了一个新的网络结构ECMA&#xff0c;从实验结果上看在超分&#xff0c;去噪&#xff0c;去模糊…

STEAM教育是什么?从学科融合到创造力培养的全新教育模式

近年来&#xff0c;“STEAM教育”成为教育界的热门词汇。随着技术和科学的发展&#xff0c;传统的单一学科教育已难以满足未来社会对创新型人才的需求。STEAM教育因其跨学科的特点&#xff0c;成为培养学生综合素质和创新能力的有效方式。那么&#xff0c;什么是STEAM教育&…

SystemC学习(3)— APB_SRAM的建模与测试

SystemC学习&#xff08;3&#xff09;— APB_SRAM的建模与测试 一、前言 二、APB_SRAM建模 编写APB_SRAM模型文件apb_sram.h文件如下所示&#xff1a; #ifndef __APB_SRAM_H #define __APB_SRAM_H#include "systemc.h"const int ADDR_SIZE 32; const int DATA_…

Spring Boot Configuration和AutoConfiguration加载逻辑和加载顺序调整

在spring中&#xff0c; AutoConfiguration也是一个种Configuration&#xff0c;只是AutoConfiguration是不能使用proxy的。 而且spring对于两者的加载顺序也不是一视同仁&#xff0c;是有顺序的。spring会先加载SpringBootApplication可达的且标注了Configuration的类&#x…

CodeS:构建用于文本到 SQL 的开源语言模型

发布于&#xff1a;2024 年 10 月 29 日 #RAG #Text2 SQL #NL2 SQL 语言模型在将自然语言问题转换为 SQL 查询&#xff08;文本到 SQL &#xff09;的任务中显示出良好的性能。然而&#xff0c;大多数最先进的 &#xff08;SOTA&#xff09; 方法都依赖于强大但闭源的大型语言…

社区交流系统设计与实现

社区交流系统设计与实现 1. 系统概述 社区交流系统是一个基于PHP和SQL的Web应用程序&#xff0c;旨在为用户提供一个互动交流的平台。该系统允许用户注册、发布帖子、回复帖子、查看其他用户的帖子和回复&#xff0c;以及管理个人资料&#xff0c;提高用户之间的互动和信息共享…

什么是字节序、大小端、高低字节、高低地址?

目录 1. 什么是字节序&#xff08;Endianness&#xff09;&#xff1f; 2. 什么是大小端&#xff08;Big-Endians and Little-Endian&#xff09;&#xff1f; 3. 什么时候需要用到大小端的概念&#xff1f; 4. 如何确认系统的大小端模式&#xff1f; 5. 什么是大小端定义…

为什么 C 语言数组是从 0 开始计数的?

C 语言等大多数编程语言的数组从 0 开始而不从 1 开始&#xff0c;有两个原因&#xff1a; 第一&#xff1a;地址计算更方便 C 语言从 0 开始的话&#xff0c;array[i] 的地址就正好是&#xff1a; (array i) 如果是从 1 开始的话&#xff0c;就是 (array i - 1) 多一次计…

锁升级及线程池相关

锁升级 在JVM底层实现锁的过程中&#xff0c;有三类锁&#xff1a;偏斜锁、轻量级锁、重量级锁 在Java6之前&#xff0c;synchronized的实现完全依靠重量级锁&#xff08;系统内的互斥锁&#xff09;&#xff0c;从用户态转为内核态非常消耗资源。在Java6之后&#xff0c;提供…

vue3+less使用主题定制(多主题定制)可切换主题

假如要使用两套主题&#xff1a;蓝色、红色 例如&#xff1a; 首先确保自己的vue3项目有less&#xff0c;这边不多做接入解释 1、在src目录下建一个styles文件夹&#xff0c;在syles文件夹下面新建两个less文件&#xff1a;theme.less和variables.less&#xff1b; theme.le…

Java面试经典 150 题.P80. 删除有序数组中的重复项 II(004)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解&#xff1a; class Solution {public int removeDuplicates(int[] nums)…

【QNAP威联通NAS系统恢复进阶教程】如果 .conf 和 md9 无法自动组装,如何恢复 NAS?

创作立场&#xff1a;原创不易&#xff0c;拒绝搬运~ hello大家好&#xff0c;我是你们的老伙伴&#xff0c;稳重的大王~ 从本期开始&#xff0c;大王将在日常教程中&#xff0c;分享一些QNAP系统故障的排除以及解决办法&#xff0c;进阶教程需要具备一定的linux基础&#xf…