面经-Unity相关
本文最后更新于:2024年9月1日 晚上
Unity生命周期
1、静态构造函数
当程序集被加载的时候就被调用了,如果你的unity处于编辑状态时,此时你保存一个脚本(从而迫使重新编译),静态构造函数会立即被调用,因为unity加载了DLL。并且它将不会再次运行,永远只会执行一次,unity运行时,是不会再次执行了!在一个已部署的游戏上,这个构造器将会在unity加载过程的早期被调用!
2、Awake
只会被调用一次,在Start方法之前被调用! 主要用于字段值的初始化工作,禁用脚本,创建游戏对象,或者 Resources.Load(Prefab) 对象
3、Start
只执行一次,在Awake方法执行结束后执行,但在Update方法执行前执行, 主要用于程序UI的初始化操作,比如获取游戏对象或者组件
4、Update
每一帧执行的,监听用户输入,播放动画,当机器忙或者性能差的时候,他会停止执行,会产生停顿的感觉,例如一个人本来在1米的位置,突然到了5米的位置上,产生了跳帧,而下面的FixedUpdate方法则相反!会一米一米的去执行!(自己调试发现,Update是先于OnGUI执行的,且执行一次Update之后,会执行两次OnGUI)
5、FixedUpdate
不管当前机器忙不忙,都会保证每一帧执行一次!避免跳帧!固定更新。固定更新常用于移动模型等操作。
6、LateUpdate
先执行Update,然后才去执行lateUpdate(Update方法执行完,必定接着执行LateUpdate,而Update和FixedUpdate方法的执行顺序不确定,而且有时候FIxedUpdate执行了多帧,而Update却只执行了一帧,这就是因为跳帧的缘故造成的(取决于你的机器性能)!),如果现在有100个脚本,分别有100个 Update()函数,其中只有一个LateUpdate,那么在同一帧中,等待100个Update()执行完后,才执行这一个LateUpdate()。
7、OnGUI
在这里面进行GUI的绘制,且GUI是每帧擦除重绘的!仅仅只是绘制!没有生命周期的概念!所有关于绘制GUI的代码,都要直接或者间接地写到OnGUI方法中!
8、OnDestroy
当前脚本销毁时调用
9、OnEnable
脚本可用时被调用、如果脚本是不可用的,将不会被调用!
11、OnDisable
如果脚本被设置为不可用将会被执行,程序结束时可用的脚本也会执行一次这个方法
OnEnable 和 OnDisable 只受脚本的可用状态的影响(enabled),而 OnBecameVisible 和 OnBecameInvisible 是受对象是否可见的影响!即使脚本设置为不可用,OnBecameVisible 和 OnBecameInvisible 也会被执行,主要是看对象是否在场景中显示了!
垃圾回收(GC)
参考:https://blog.csdn.net/worisaa/article/details/64121436
堆栈内存存储较小的数据和时间维度较短的,堆内存相反。
- 只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态,变量要么存储在堆栈内存上,要么处于堆内存上。
- 一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于堆栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,其对应的内存依然会被标记为使用状态。
堆内存分配和回收机制
堆内存上的内存分配和存储相对而言更加复杂,主要是堆内存上可以存储短期较小的数据,也可以存储各种类型和大小的数据。其上的内存分配和回收顺序并不可控,可能会要求分配不同大小的内存单元来存储数据。
堆上的变量在存储的时候,主要分为以下几步:
- 首先,unity检测是否有足够的闲置内存单元用来存储数据,如果有,则分配对应的内存单元;
- 如果没有足够的存储单元,unity会触发垃圾回收来释放不再被使用的堆内存。这步操作是一步缓慢的操作,如果垃圾回收后有足够的内存单元,则进行内存分配。
- 如果垃圾回收后并没有足够的内存单元,则unity会扩展堆内存的大小,这步操作会很缓慢,然后分配对应的内存单元给变量。
堆内存的分配有可能会变得十分缓慢,特别是需要垃圾回收和堆内存需要扩展的情况下。
垃圾回收时的操作
当一个变量不再处于激活状态的时候,其所占用的内存并不会立刻被回收,不再使用的内存只会在GC的时候才会被回收。
每次运行GC的时候,主要进行下面的操作:
- GC会检查堆内存上的每个存储变量;
- 对每个变量会检测其引用是否处于激活状态;
- 如果变量的引用不再处于激活状态,则会被标记为可回收;
- 被标记的变量会被移除,其所占有的内存会被回收到堆内存上。
GC操作是一个极其耗费的操作,堆内存上的变量或者引用越多则其运行的操作会更多,耗费的时间越长。
何时会触发垃圾回收
主要有三个操作会触发垃圾回收:
- 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存;
- GC会自动的触发,不同平台运行频率不一样;
- GC可以被强制执行。
降低GC的影响的方法
大体上来说,我们可以通过三种方法来降低GC的影响:
- 减少GC的运行次数;
- 减少单次GC的运行时间;
- 将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC
基于此,我们可以采用三种策略:
- 对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。
- 降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存碎片。
- 我们可以试着测量GC和堆内存扩展的时间,使其按照可预测的顺序执行。当然这样操作的难度极大,但是这会大大降低GC的影响
降低GC的具体方法
减少内存垃圾
缓存
比如循环不要每次都创建新的数组,而是提前创建好
不要在频分调用的函数里面使用堆内存分配
比如tick和update里面不要分配内存,而是在之前就创建好,或者通过定时器或者脏标记的方法来改进
减少链表
对象池
减少内存的释放和创建次数
造成不必要内存的分配的情况
字符串
在c#里面字符串不是值类型而是引用,而且值无法改变,所以改变值会直接创建一个新的值
1)减少不必要的字符串的创建,如果一个字符串被多次利用,我们可以创建并缓存该字符串。
2)减少不必要的字符串操作,例如如果在Text组件中,有一部分字符串需要经常改变,但是其他部分不会,则我们可以将其分为两个部分的组件。
3)如果我们需要实时的创建字符串,我们可以采用StringBuilderClass来代替,StringBuilder专为不需要进行内存分配而设计,从而减少字符串产生的内存垃圾。
4)移除游戏中的Debug.Log()函数的代码,尽管该函数可能输出为空,对该函数的调用依然会执行,该函数会创建至少一个字符(空字符)的字符串。如果游戏中有大量的该函数的调用,这会造成内存垃圾的增加。
函数调用
比如说迭代器会产生新的数组(可以通过缓存来解决),调用GameObject.name 或者 GameObject.tag也会有内存垃圾,因为会犯乎一个字符串
装箱操作
装箱操作是指一个值类型变量被用作引用类型变量时候的内部变换过程,如果我们向带有对象类型参数的函数传入值类型,这就会触发装箱操作。比如String.Format()函数需要传入字符串和对象类型参数,如果传入字符串和int类型数据,就会触发装箱操作。最好避免。
比如这种
1 |
|
协程
函数引用
foreach
6.5之前的版本会因为迭代器有内存垃圾
重构
比如把对象里面的string拆出来,这样就不用频繁地在GC里面类型检查
定时执行GC
比如过场的时候主动调用GC操作
UI
ugui
rebatch和rebuild
Canvas(画布),和名字一样,是 UI 绘制的地方,Unity 的渲染系统用其来提供一个可绘制的分层几何。负责将ui几何合批成适合的网格,提交绘制命令给 Unity 的图形系统,这整个过程叫做 rebatch 或者 batch build。当 Canvas 其子节点下包含 Canvas Renderer 的节点需要进行 rebatch 的时候,就会被标记为脏。
Graphic 与 Layout 都依赖于 CanvasUpdateRegistry 类。它会定位 Graphic 与 Layout 是否需要更新并加入更新队列,在所在 Canvas 的 willRenderCanvases 事件被触发时对队列中的对象执行真正的更新。
图集有什么用,适用场景。
定义
图集是将很多零碎的2D小图整合成一张大图,方便unity渲染合批,降低渲染消耗。
优势
1.UI的合批处理,减少DrawCall
多张图片需要多次DrawCall,合并成一张大图只需要调用一次DrawCall
2.减少对内存的占用
OpenGL每张贴图都需要设置为2的N次方才能使用,假设有宽高分别为100x100、10x10的两张图片,如果不合成大贴图,那么就需要分别使用128x128和16x16的图片,会浪费一部分内存空间。
如果是使用一张大图的话,就可以将两张图片打到128x128的图集,进而减少内存的占用。
3.提升效率
图片尺寸为2的次幂时,GPU处理起来会快很多,小图不可能做不到每张图都是2的次幂的,但打成一张大图就可以。
https://blog.csdn.net/z2014z/article/details/119654256
屏幕自适应方案
canvas scaler
锚点
批处理和drawcall
Unity渲染优化的4种批处理:静态批处理,动态批处理,SRP Batcher 与 GPU Instancing - acnestis的文章 - 知乎
常用的Unity Atrribute
笔试题
unity脚本生命周期函数。
Lerp作用
欧拉角计算,Quaternio.Euler。
常用的Unity Atrribute
如何判断是否碰到了场景中的物体?
Coroutine有什么用,优缺点
发生碰撞的必要条件。
ugui如何保证分辨率改变ui大小不变。
C#List底层数据结构,增加和删除时间复杂度。
结构体和类的区别。
引用类型和值类型分别有哪些。
装箱和拆箱是什么,为什么不要频繁适用?
简单用代码实现一个单例模式。
观察模式读代码写结构,用在哪里?
频繁地对字符串进行分割,写一个代码。(就是分数比如说100000,分成100,000这样)
分批手段和适用场景。