Unity的八股收集(持续更新)
Unity的八股收集(持续更新)
前提说明:答案都是个人的理解 + 网络搜寻,不一定正确,请谨慎参考!
我个人认为面试回答应该尽量精简一些,防止被面试官追问太多,但是这样做可能也会给面试官留下不好的印象。所以才有回答中的扩展。大家可以根据自己的能力来挑选部分扩展的内容进行回答。如果自身能力不行,我建议还是不要展开太多扩展内容,以免给面试官机会被其追问。
有些面试题非常的基础,比如关于数据结构方面的知识。对于多年的程序员而言,我不建议大家把时间花费在这些基础知识上,因为这些知识在实际面试一般不会再提及了,当然你要是想巩固知识倒是可以看看。
下面的代码中,有部分我会标记为答案存疑,这是因为我个人感觉这有点问题,但是我也找不到权威的资料证明。那些没有被标记的答案实际上也不能说是百分百的正确,只能说是我个人感觉这样回答是没问题的。
通用概念
Q: 面向对象的三大特点?
答:
1. 继承。允许新类基于已有的父类进行扩展,子类可以直接复用父类的属性和方法。 1.1 继承有传递性。传递机制 a继承b; b继承c; c具有a的特性 。 1.2 补充:继承的单根性。在C#中一个类只能继承一个类,不能有多个父类。
2. 封装。将数据和对数据的操作绑定在一起,隐藏内部实现细节,仅对外暴露必要的访问接口。
3. 多态。同一个方法在不同的子类对象上可以呈现不同的执行效果,通过统一接口操作不同对象。
追问:-
Q: 这些有什么作用呢?
答:
1.继承。提高代码重用度,增强软件可维护性的重要手段,符合开闭原则。实现代码的层级化组织,避免重复编写公共逻辑,同时为多态特性提供实现前提。
2. 封装。提升代码安全性,降低模块耦合度,内部逻辑修改不影响外部调用,大幅提升可维护性。
3. 多态。提升代码的灵活性和可扩展性,适配开发中大量不同子类的统一调度场景。
Q: 内存中,栈和堆的区别是什么?
答:
栈内存是为线程留出的临时空间,而且栈空间存储的数据只能由当前线程访问,所以它是线程安全的。其用于存储局部变量和方法调用,由系统自动管理,栈中数据的生命周期随着函数的执行完成而结束。
堆内存用于存储动态分配的对象,一般由程序员分配和释放。堆内存可以被一个进程内所有的线程访问,它是线程不安全的。如果我们没有对其进行释放则会造成内存泄漏的问题。Q: 序列化是什么?
答:
专业解释:把对象转换为字节序列的过程称为对象的序列化。
通俗解释:在代码运行过程中,我们想将对象中的信息转换为某种数据格式下的信息,这个过程称为序列化。
追问:-
Q: 常见的序列化方式有哪些?
答:
常见的序列方式有 json格式,xml格式,二进制等。 -
Q: 什么时候我们会用到序列化?
答:
当我们想将对象中的信息存储下来或是进行网络传输的时候。
Q: 深拷贝和浅拷贝的区别?
答:
浅拷贝只复制对象的第一层字段,如果这个字段是引用类型(如数组、类对象),复制的是引用地址,不复制引用指向的内容。
深拷贝的话,不仅会复制地址,还会复制所有引用字段所指向的内容,相当于创建了一个完全独立的副本。
追问:-
Q: 分别在哪个场景下使用?
答:(下面纯粹是个人想法)
如果赋值的内容是只读的,或是说它的修改并不影响整体逻辑,那么可以使用浅拷贝。反之则使用深拷贝。
Q: 简单描述帧同步跟状态同步的区别?
答:
帧同步:
原理:所有玩家的客户端在相同的时间步长内,根据相同的输入执行游戏逻辑。即各个客户端通过交换每一帧的玩家指令,然后利用确定性的游戏逻辑自行计算下一帧的状态。
优点:只要游戏逻辑完全确定,所有客户端计算出的结果就能完全一致,从而保证游戏状态的一致性。
缺点:对网络延迟非常敏感。如果某个玩家有输入延迟了,可能导致游戏体验下降;另外,要求所有玩家的计算算环境一致,否则容易出现同步偏差。
状态同步:
原理:服务器作为权威端,负责计算和维护游戏的整体状态,并周期性地将状态更新(或快照)发送给各个客户端。客户端在接收到代态后,根据本地渲染和预测算法进行平滑过渡(如插值和预测)。
优点:服务器主导的方式能够更好地应对网络抖动和延迟问题,客户端无需完全依赖彼此的输入,从而提高了网络环境较差时的稳定性。
缺点:可能出现由于延迟而导致的状态不一致或画面抖动问题另外,需要额外的插值和预测算法来平滑状态过渡,设计和调试较为复杂。
参考资料:
Q: 简述进程、线程、协程的概念?
答:
进程(Process)
- 它是正在运行程序的实体,拥有独立的虚拟内存空间、文件描述符等资源。一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),进程是操作系统资源分配的基本(或者说“最小”)单位。
线程(Thread)
- 线程从属于进程,也被称为轻量级进程,是程序的实际执行者。线程是操作系统CPU调度的基本单位(其他说法:线程是操作系统能够进行运算调度的最小单位;线程是进程中执行运算的最小单位)。它是进程内部的一条执行流,同一个进程内的多个线程共享该进程的内存空间(堆、全局变量等),但各自拥有独立的栈和寄存器。
协程
- 协程是用户态的轻量级执行单元,又称“微线程”。它完全由应用程序(或语言运行时)自己调度,不依赖操作系统内核。
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 归属单位 | 操作系统资源分配 | 操作系统CPU调度 | 用户态程序调度 |
| 内存共享 | 独立地址空间 | 共享进程内存空间 | 共享线程内存空间 |
| 切换速度 | 最慢 | 中等 | 最快 |
| 调度方式 | 内核抢占式 | 内核抢占式(时间片) | 用户态协作式(主动让出) |
| 典型应用 | 多开App、浏览器标签 | 多核计算、后台服务 | 高并发IO(如网络爬虫、Web服务) |
-
Q: 进程与线程的区别?
答:
调度:线程作为处理器调度和分配的基本单位,而进程是作为拥有资源的基本单位。
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
拥有资源:进程是拥有资源的一个独立单位,有自己独立的地址空间;线程不拥有系统资源,但可以访问隶属于进程的资源,共享进程的地址空间。
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
-
Q: 线程与协程的区别?
答:
线程:由操作系统内核管理,是抢占式调度的系统级工人。
协程:由应用程序运行时管理,是协作式调度的“用户级临时工
详细对比表:PS:Deepseek提供
对比维度 线程 (Thread) 协程 (Coroutine) 归属层级 内核态(OS级别) 用户态(Runtime/语言级别) 调度方式 抢占式(时间片用完,内核强制切换) 协作式(主动让出,如 yield/await)创建/切换成本 较高(需陷入内核态,保存大量寄存器及栈) 极低(仅用户态保存少量上下文,无系统调用) 栈内存大小 固定且较大(通常 1~2MB,不可随意扩容) 极小(通常 2~64KB,动态可伸缩) 并发数量级 数千个(受限于系统内存和内核切换开销) 数十万甚至百万个(内存占用小,切换快) 阻塞影响 阻塞整个线程(如执行 sleep或 I/O 时,线程被挂起,浪费CPU)仅阻塞当前协程(遇到 I/O 主动让出,线程跑去执行其他协程,CPU利用率高) 多核利用 天然支持(多核可并行执行不同线程) 默认单核并发(需配合多线程/多进程才能实现真正的并行) 数据竞争风险 高(共享内存,需频繁使用互斥锁 Mutex保护)较低(通常配合无锁编程或 Channel 通信,但若共享变量仍需加锁) 调试与堆栈 调试成熟,堆栈信息完整清晰 堆栈信息较浅,调试相对复杂(部分语言堆栈不完整)
参考资料:
Q: 什么是线程阻塞?其可能原因有哪些?
答:
线程阻塞是指线程进入等待状态,无法继续执行,只有等到满足某个条件才可继续执行。
常见的原因有: 1. 线程执行IO操作,比如文件读写,网络通信,数据库查询。
互斥锁阻塞:当多个线程访问共享数据时,如果互斥锁被另一个线程占用,其他线程就会进入阻塞状态。
线程休眠。
高级优先级线程长期占用CPU过多,低优先级线程无法获得执行机会。
Q: 线程安全指的是什么?
答:
线程安全指的是在多线程的代码环境下,同时访问共享资源并且操作时候,不会产生不正确的结果或者不可预期的错误。
追问:-
Q: 如何避免?
答:
可以使用一些线程锁机制来避免。
Q: 单例模式的优缺点?
答:
优点:
全局数据共享。
确保唯一性。
方便管理资源。
方便管理对象。
访问简单化。
便于扩展。
缺点:
单例模式可能会造成内存泄漏。
有一些设计存在的线程安全问题。
单例模式的类看起来是独立的,其实有可能写不好会有隐藏的依赖关系存在。
Q: 单例模式中为什么要加锁,是否为必须加锁?
答:
单例模式加锁,是为了确保在多线程环境下只有一个实例被创建,解决线程的安全问题。如果在项目中,不存在多线程同时访问单例情况的话,那么可以不加锁。
Q: 观察者模式在游戏开发中一般有什么作用?
答:
观察者模式可以制作事件监听和触发,可以让游戏系统之间松耦合,提高代码维护性和拓展性。当一个对象状态改变时,可以将监听它的对象会收到通知执行对应的逻辑。
Q: 渲染管线是什么?
答:
渲染管线(Rendering Pipeline)是将3D场景数据转化为最终为2D屏幕图像的处理流程。总体可以划分为四个功能性阶段:应用阶段(Application)、几何阶段(Geometry Processing)、光栅化阶段(Rasterization)和像素阶段(Pixel Processing)。
Q: Batch、Set pass call、 Draw call 分别是什么含义?
答:
Draw call:是Unity推送到GPU命令缓冲区的少量字节,简单来说就是一条指令,告诉GPU可以开始绘制了,如果这个数量多的话,CPU就会排队执行就会造成卡顿,它是直接影响渲染性能,因为它的数量决定了CPU与GPU的通信次数。
Set Pass Call:渲染Pass(shader中的渲染通道)的数量,移动平台中尽量减少这个能提升性能。
Batch:一次Batch指的是CPU将顶点数据加载到显存,然后设置渲染状态(使用哪个顶点/片元着色器,用什么纹理光照混合模式去渲染),最后调用Draw Call,它是一种将多个相似的物体合并成一个Draw Call的渲染优化技术,可以减少CPU与GPU之间的数据传输和渲染开销。
Q: 请描述游戏动画有哪几种,以及其原理?
答:
- 精灵动画 (Sprite Animation):最经典直接的2D动画方式。像翻书一样,按顺序快速播放一系列静态图片(精灵图),形成动态效果
- 刚体分层动画 (Rigid Hierarchical Animation,又称关节动画):将角色分成头、胳膊、腿等多个不会变形的独立部分(刚体)。动画只记录这些刚体的位置和旋转角度。缺点是关节连接处容易产生“撕裂感”。可视为3D骨骼动画的雏形。
- 顶点动画 (Vertex Animation):直接记录模型每个顶点在每一帧的位置变化。
- 骨骼蒙皮动画 (Skeletal Animation / Skinned Mesh Animation):现代3D游戏最主流的方式。其核心是“骨骼”和“蒙皮”。它解决了刚体动画的“撕裂”问题,又比顶点动画更高效。
- 骨骼
(Skeleton):由一系列关节(Joint)组成的树状结构。动画师通过旋转关节来摆出各种姿势。
- 蒙皮 (Skin):角色的3D模型(网格)附着在骨骼上。每个顶点被赋予权重,绑定到一个或多个骨骼上。
Q: Protobuf跟Json,protobuf优化在哪?
答:(下面答案存疑)
Protobuf 使用二进制编码,使用数字标签替代json的字段名,数据格式使用tlv(tag-length-value)结构,tag中包含字段号和数据类型,length数据长度,value字段具体的值。数据结构
Q: 什么是哈希表?
答:
哈希表(Hash Table)是一种高效的数据结构,用于实现键值对(Key-Value Pair)的存储和快速检索。它通过哈希函数(Hash Function)将键(Key)映射到一个固定存储结构中(称为哈希桶或槽,一般是使用数组)中的某个位置。
追问:
-
Q: 什么是哈希函数?
答:
哈希函数是一种将任意大小的数据映射为固定大小值(通常是整数或二进制序列)的函数。 -
Q: 你知道什么是哈希冲突吗?
答:
哈希冲突指的是对于不同的两个数据,哈希函数计算出来的结构是一致的。这就导致这两个数据要争抢两个相同的存储位置,这就是哈希冲突。 -
Q: 如何解决哈希冲突?
答:
-
1.开放寻址法:当发生哈希冲突时,会寻找另一个空闲的哈希地址来存储当前元素。这种方法的具体实现有多种,这么我们只说明线性探测法和平方探测法(二次探测)。
- 线性探测法:当目标位置已被占用时,顺序查找下一个空闲位置。
- 平方探测法:与线性探测法类似,但是探测的步长是平方的。这样可以在前后两个方向上寻找空闲位置。
- 2.链地址法:将所有具有相同哈希值的元素链接在一起,形成一个链表。当发生哈希冲突时,将新的元素插入到链表的尾部。这种方法查找的平均时间为O(n),这个n是链表的大小。
- 3.再哈希法:再哈希法涉及构造多个不同的哈希函数。当发生哈希冲突时,使用第二个、第三个等其他哈希函数计算新的地址,直到找到一个空闲位置为止。这种方法可以减少聚集现象,但会增加计算时间。
- 4.建立公共溢出区。将哈希表分为基本表和溢出表两部分。基本表用于存储未发生冲突的元素,而溢出表用于存放所有发生冲突的元素。这种方法可以简化冲突解决过程,但可能会影响查找效率。这种方法有点类似于链地址法,有些人会使用红黑树来做表。
-
1.开放寻址法:当发生哈希冲突时,会寻找另一个空闲的哈希地址来存储当前元素。这种方法的具体实现有多种,这么我们只说明线性探测法和平方探测法(二次探测)。
Q: 数组优缺点?
答:
优点:
- 数组可以较快的索引并且修改或赋值单个元素也很简单。
- 天然支持多维
缺点: 1. 插入数据十分的麻烦。我们需要先将插入点后的元素全部赋值为其前一个元素的值,然后我们再将数据赋值给插入点。整体的时间复杂度为O(n),其中n是插入点后面的元素个数。(PS:如果你无需保障数组数据的顺序性,你完全可以先将要被覆盖的值先赋值于数组中最后非空余元素的后一位元素。然后在将要插入的数据赋值给插入点) 2. 数组的长度一开始就定下来了,这在一定程度上限制了我们的使用。
相关资料:计算机网络
Q: TCP和UDP的区别?
答:
TCP是传输控制协议,提供的是面向连接的、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发、丢弃重复数据、校验数据、流量控制等功能,保证数据能从一端传递到另一端。
UDP是用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传递给IP层的数据报发送出去,但是并不保证它们能够到达目的地。由于UDP在传输数据报之前不用在客户端和服务器之间建立一个连接,而且没有超时重发等机制,故传输速度很快。
Q: 如果要让UDP通讯具备TCP的优点,应该如何处理?
答:
可以在UDP通讯的基础上增加模拟出TCP的安全性。比如可以在消息中加入序号,可以加入消息确认机制,接收端在接收到之后向发送端发送一个确认消息,可以加入超时重传机制。
Q: TCP分包(拆包)与粘包是什么?
答:
TCP中的分包拆包指的是一条消息数据包过大,TCP自动拆成多个消息数据包发送。
粘包指的是多条短小的数据消息,被合并为一个消息数据包进行发送了。
追问:
-
Q: 如何解决粘包问题?
答:
可以在每个消息包加入消息头,这个消息头内包含数据的长度,当接受到消息的时候,可以通过这个长度来分包得到正确的数据。
CSharp相关
Q: .Net与 Mono 的关系?
答:
.Net是一个语言平台,Mono为.Net提供集成开发环境,集成并实现了.NET的编译器、CLR 和基础类库,使得.Net既可以运行在Windows也可以运行于 linux,Unix,Mac OS 等平台。
Q: JIT和AOT区别?
答:(下面答案由Deepseek提供)
JIT(Just-In-Time,即时编译):C#代码首先被编译成中间语言(IL,即MSIL),生成 .dll 或 .exe。当程序运行时,.NET 运行时(CLR)会按需,将正在执行的那部分 IL 代码实时编译成当前操作系统和CPU能理解的机器码。编译结果会被缓存起来,下次执行同一段代码时直接使用缓存,不再重复编译。
AOT(编译时完成):在发布/部署之前,就将所有 IL 代码直接编译成目标平台(如 Windows x64、Linux ARM64)的机器码,生成一个自包含(Self-Contained)的本机可执行文件。运行时无需再进行任何编译操作。
| 对比维度 | JIT(即时编译) | AOT(提前编译) |
|---|---|---|
| 启动速度(冷启动) | 劣势:较慢。启动时需要加载JIT编译器,并将入口代码实时编译为机器码。 | 优势:极快。代码在运行前已是原生机器码,直接映射到内存执行,近乎零延迟。 |
| 长期运行峰值性能 | 优势:更优。运行时可根据实际CPU指令集(如AVX-512)和运行数据进行动态优化(PGO),长期运行后性能往往超过AOT。 | 劣势:中等。仅能做静态编译优化,无法根据运行时硬件情况做激进调整,性能稳定但上限略低于JIT。 |
| 内存占用 | 劣势:较高。除程序本身外,还需额外占用内存存放JIT编译器本身以及动态生成的机器码缓存。 | 优势:较低。无需加载JIT编译器,且无需保留IL元数据供编译,运行时内存占用更小。 |
| 最终发布包体积 | 优势:较小。通常依赖系统或共享的.NET运行时,发布包只包含IL中间代码,体积仅几MB。 | 劣势:巨大。需将完整的.NET运行时(GC、线程管理、异常处理等)静态链接进可执行文件,单文件可能达几十甚至上百MB。 |
| 动态代码与反射支持 | 优势:完整支持。完美支持
System.Reflection.Emit、动态创建类型、动态生成委托等运行时元编程操作。 |
劣势:受限。编译器会提前移除部分元数据和IL代码,不支持运行时动态生成新代码,反射功能需配合“裁剪器”注解才能正常使用。 |
| 跨平台分发灵活性 | 优势:高。只需编译一份IL的DLL,即可在Windows、Linux、macOS等多个平台上由各自的JIT解释运行。 | 劣势:低。一份编译好的AOT二进制文件只能对应一个特定的操作系统和CPU架构(如linux-x64),需为每个目标平台单独构建发布包。 |
Q: C#中所有引用类型的基类是什么?
答:
引用类型的基类是System.Object,值类型的基类是 System.ValueType。同时,值类型也隐式继承自System.Object。
Q: 什么是值类型与引用类型
答:
值类型包含简单数据类型,结构体类型和枚举类型。
引用类型包括类,string字符串,数组,接口等。
扩展: 简单数据类型:int、long、byte、float等。
追问:-
Q: 值类型与引用类型的区别?
答:
从程序运行中的存储方式来看,我们一般认为值类型在存储在栈上,而引用类型存储在堆上。但是实际上还是要看具体的情况而定,比如定义在类中的值类型,其存储位置仍然在堆上。
从赋值方式来看,值类型的赋值是复制对应的信息,而引用类型变量的赋值是复制对象的引用。比如值类型a和b。当我们写代码a=b;此时a会复制b中所有的信息。但是a和b所指向的地址是不一样的。如果是a,b是引用类型,此时a和b所指向的地址就是一致的。
Q: 结构体和类的区别?
答:
- 结构体是值类型,类是引用类型。
- 一般情况下认为结构体存在栈中,类存在堆中。
- 结构体变量和类对象进行数据传递时,结构体变量进行的就是值传递,而类对象进行的是引用传递,或者说传递的是指针。
- 在C#中结构体类型定义时,成员是不能初始化的,这样就导致了,定义结构体变量时,变量的所有成员都要自己赋值初始化。但对于类,在定义类时,就可以初始化其中的成员变量,所以在定义对象时,对象本身就已经有了初始值,你可以自己在重新给个别变量赋值。(注意在C++中,类的定义中是不能初始化的,初始化要放在构造函数中)
- 结构体不能申明无参的构造函数和析构函数,而类可以。
- 声明了结构类型后,可以使用new运算符创建构造对象,也可以不使用new关键字。如果不使用new,那么在初始化所有字段之前,字段将保持未赋值状态且对象不可用。
- 结构体不能被继承,而类可以。
- 结构体需要在构造函数中分配所有成员变量,而类随意。
- 结构体不能被静态static修饰(不存在静态结构体),而类可以。
-
Q: 分别讲述一下对应的使用场景?
答:
结构体:
- 轻量级的数据集合可以优先考虑接结构体(比如位置,颜色等)
- 在变量传值的时候,希望传递对象的是拷贝,而不是对象的引用地址,这个时候就可以使用结构体。
类:
- 如果对象需要用到面对对象的三大特性,则优先考虑类。
- 除了数据存储外,还需要一些复杂的逻辑处理,则优先考虑类。
- 在变量传值的时候,希望传递对象的不是拷贝,而是对象的引用地址,这个时候就可以使用类。
Q: 什么是装箱与拆箱?
答:
装箱,是将值类型转换成引用类型。
拆箱,是将原本装箱的引用类型转换成值类型。
追问:-
Q: 哪个更耗性能,为什么?
答:(下面答案存疑)
在装箱时,系统会在堆上分配一个内存,从栈上把这个值复制到堆上。
在拆箱时,会在栈上分配一块内存,从堆上将值复制过来。
因为装箱会从堆上分配内存,所以装箱更耗性能。
Q: C#中四种访问修饰符是什么?
答:
- public(公开的):访问不受限制。任何类、任何程序集中的任何代码都可以访问被标记为 public 的类型或成员。
- private(私有的):访问范围最小。仅限包含该成员的同一个类内部访问,子类也无法继承访问。
- protected(受保护的):访问仅限于包含该成员的类,以及派生自该类的子类(无论子类是否在同一个程序集中)。
- internal(内部的):访问仅限于同一个程序集(Assembly)内的任何类。在不同程序集(如外部引用的DLL)中,即使是子类也无法访问。
Q: override与new的区别?
答:(下面答案存疑)
override 是实现多态的关键字,是为了改变基类中虚拟或者抽象函数的行为。
new则是在派生类中定义一个与基类相同名字的函数。Q: C#中的==和Equal的区别?
答:
==是运算符,而Equals是虚方法。
扩展:
C# 中有两种不同的相等:引用相等和值相等。值相等是大家普遍理解的意义上的相等:它意味着两个对象包含相同的值。例如,两个值为 2 的整数具有值相等性。引用相等意味着要比较的不是两个对象,而是两个对象引用,这两个对象引用引用的是同一个对象。
如果要实现大家普遍理解意义上的相等,无论是==还是Equal都应该进行重写。如果你重写了==,那么在判断是否为相同引用时,你要么使用Object.ReferenceEquals进行判断,要么在重写的时候考虑到同引用的判断。
相关资料:
Q: C#中的接口的作用是什么?
答:
接口是一种抽象机制,其作用是定义契约而不提供具体实现。它可以统一处理不同对象的多态性,不同对象有相同行为时,可以使用接口来对不同对象的行为进行整合。Q: C#中数组跟List在什么时候用更合适?
答:
在不需要复杂功能、数据长度固定或者追求性能的情况下使用数组,其他情况则使用List会更加方便。
扩展:
List优缺点:
优点: 1. 允许通过指定泛型类或方法操作的特定类型,而泛型功能将类型安全的任务从您转移给了编译器。不需要编写代码来检测数据类型是否正确,因为会在编译时强制使用正确的数据类型。减少了类型强制转换的需要和运行时错误的可能性。泛型提供了类型安全但没有增加多个实现的开销。
常见的操作都已经封装完毕。
数据长度可任意更改。
缺点: 1. list原本一维。
对应的相关资料:Q: C#中的委托与事件的区别是什么?
答:(下面答案存疑)
委托本质上是一种引用方法的类型,它可以作为回调函数以及传递方法作为参数使用,可以在类的外部被直接调用和赋值,用于存储和调用方法。
事件的话是基于委托而封装的一种发布-订阅模型,在类的外部只能使用订阅或者取消订阅来使用,事件遵循观察者设计模式,使得对象之类可以松耦合。
个人想法:上面的回答是从网络上抄取的答案。但是我个人并不这么认为。委托是一些语言提供的方法。而事件是一种概念,是一类行为的总称。这个问题就很奇怪,我认为这两样东西首先要有相同点,然后再考虑区别才是。
Q: C#中有一个常用的接口叫做IDispose,它的作用是什么?
答:
IDispose的作用是用于手动释放资源的机制。程序员通过显式调用IDispose中的方法来实现资源的释放,避免资源泄漏和浪费,它允许对象在不需要的时候释放资源,而不依赖于自动回收机制。Q: C#中有垃圾回收机制,置为空的时候系统会回收,那为什么还要有IDispose接口?
答:(下面答案存疑)
C#中的垃圾回收机制,只会回收处在托管堆上分配的内存对象,那么在非托管堆上的对象,垃圾回收机制无法自动处理,因此就需要手动的释放这类资源,而IDispose就是提供了一种显式释放资源的机制,主要用于释放非托管堆上的资源。Q: 一个类中的函数会占用这个类对象的内存空间嘛?
答:
不会,函数只是一堆指令的集合,是存储在类的元数据中。Q:C#中的垃圾回收机制的实现方式?
答:(下面答案存疑)
C#中的垃圾回收机制是使用分代+标记+清除+压缩的方式。
分代收集内存。
从根对象沿着引用图遍历引用对象,如果还活跃的就标记。没被标记的对象为可清除的对象,之后会清理掉。
清除即释放未存活的对象内存。
压缩,主要是为了解决内存碎片问题,整理内存空间,让存活对象顺序排列,将存活的对象从堆的一侧移动到另外一测,整理出连续的空闲内存块。
Q: C#中触发自动GC的原因有哪些?
答:(下面答案存疑)
C#的垃圾回收机制是使用分代,在分代方面的话,它是分成了三代0、1、2。
0代的作用是新对象的存储。
1代的作用是从0代存储幸存下来的。
2代的作用是存储大数据和长期保存的数据。
自动触发的原因是:
当0代满了之后会触发。
当对象分配到1代的时候,1代空间不足了会触发1代的垃圾回收。
如果2代满了,会触发全代的垃圾回收。
Q: 如何避免GC过多?
答:(下面答案存疑)
尽量减少new对象,可以使用对象池复用。
在某些场景下用stringbuilder替换string,避免字符串拼接时产生的GC。
公共对象使用静态声明。
尽量避免装箱操作。
尽量不使用Linq,因为会产生大量的迭代器跟匿名类型。
Q: String 跟StringBuilder区别?
答:(下面答案存疑)
String是字符串常量,线程安全。String类型是一个不可变的对象,所以当每次要对string对象进行改变时,都得重新生成一个新的string对象,然后将指针指向这个新的对象。
StringBuilder是字符串变量,线程不安全。StringBuilder对象在做字符串连接操作时是在原来的字符串上进行修改,改善了性能
如果在一个for循环里操作,就要不断生成新对象,很废性能,所以建议string如果在不断更改的话使StringBuilder。Q: C#中new一个引用对象做了什么?
答:(下面答案存疑)
当我们使用C#去new了一个引用对象时,其会创建实例,分配内存,初始化字段,调用构造函数并返回引用。Q: C#中的反射是什么?
答:(下面答案存疑)
因为CSharp允许程序在运行时检查、访问和操作类型、成员以及程序集的元数据(请注意部分平台禁止了CSharp这样的操作)。而反射使得程序可以动态地加载类型,调用方法,创建对象,获取和设置属性,而不需要在编译的时候知道这些类型的具体信息。Q: C#中的闭包指的是什么?
答:(下面答案存疑)
闭包是指一个函数,通常是匿名函数或者lambda表达式能够捕获并记住所在作用域中的变量,并在后续调用中继续访问和修改这些变量的能力,核心是将函数与其引用的外部变量绑定在一起。Q: 为什么C#中的闭包容易造成内存泄漏?
答:(下面答案存疑)
因为闭包是在匿名函数中会将外部的变量绑定在一起,持有这个变量的引用,这些引用会导致外部变量在不需要的时候也不会被回收。Q: C#中堆内存如何优化?
答:(下面答案存疑)
减少临时对象的分配。
对于小而简单的数据结构类,可以使用结构体替代。
尽量避免频繁分配和释放大对象。
避免内存泄漏。
Unity相关
Q: 介绍一下AB包。
答:
AB包(AssetBundle)是Unity提供的一种资源压缩包,用于高效管理和加载游戏资源。通过将资源分布在不同的AB包中,可以显著减少运行时的内存压力,并支持动态加载和卸载资源。Q: 介绍一下AA包。
答:
AA包(Addressables)是Unity提供的一种资源管理系统,它允许开发者通过资源的地址来请求资源。这个系统提供了一种更灵活、更高效的方式来管理和加载游戏中的资源,无论资源是本地存储还是远程服务器上。此系统的引入,使得资源的动态加载和内存管理变得更加简单和高效。Q: AB包有几种打包压缩方式?
答:
不压缩,LZMA,LZ4三种。
追问:
-
Q: 请问LZMA跟LZ4有什么区别?
答:
LZMA:
.LZMA采用流压缩方式(stream-based),压缩率会比LZ4更高,体现在包体更小,但是问题也很严重。LZMA只支持顺序读取,所以加载AB包时,需要将整个包解压,会造成卡顿和额外内存占用。
加载AB包后将所有资源进行了缓存,导致了如果AB包资源利用率在短时间利于率不高的时候,造成了很高的内存浪费。
一套引用计数规则非常复杂,当资源过多的时候建立引用关系都是很费时的,其中的常驻包的设置逻辑也是非常具有不确定性。
LZ4:
LZ4采用块压缩方式(chunk-based),块压缩的数据被分为大小相同的块,被分别压缩,虽然压缩率不及LZMA,但是读取效率比LZMA高非常多
LZ4压缩的AB包,使用LoadFromFile()或LoadFromStream()只会加载AB包的Header,相比于直接加载解压整块AB包,效率更进一步提高。另外一个很重要的点,由于可以只加载Header,因此AB包可以做到一旦加载到内存后就再不卸载,此时只需要管理从AB包中读取出来的资源的生命周期。
对于之前使用引用计数的优化,由于Unity原本资源管理就是使用引用计数去维护,这里再建立一套内部的引用计数,不仅多余而且很浪费CPU资源,而且效果不一定很好。这个时候我们可以建立一套弱引用管理体系,通过弱引用去持有资源,在触发Resource.UnloadUnusedAssets()再去清除弱引用失效的对象。
Q: Unity的协程与C#的线程、进程有什么区别?
答:
协程:由程序调度的,切换成本比较低,是在主程序运行过程中,同时通过编译器生成状态机类来控制协程的执行。状态机是一种用来描述协程的状态变化和控制流的结构,利用异步操作实现挂起和恢复,这样可以使协程在不影响主线程运行的同时,实现复杂的控制流和状态管理。适合对某任务进行分时处理。
线程:线程是CPU调度的最小单位,是由操作系统调度的,切换成本高,可以同一时间同时执行多个线程,线程适合多任务同时处理,并发并行。Unity要求除主线程之外的线程无法访问Unity3D的对象、组件、方法。
进程:进程是资源分配的最小单位,是一个独立的程序,拥有自己的内存、资源,一个进程可以包含很多线程,进程终止后,线程也会终止。
更多关于线程和进程的问题,请点击跳转至进行和线程的问题Q: 请解释Unity中的Prefab是什么,以及它在游戏开发中的作用是什么?
答:
在Unity中,Prefab是一种对象的预制体或者模板,以便我们可以在需要的时候以其为模板实例化多个相同的对象,可以轻松的创建和管理。在游戏开发中,因为可以创建可重用的对象,所以可以在开发中进行快速的迭代与更改,而不需要重复编写代码,Prefab还可以预览和调整对象上的属性以及组件,方便测试和修改,从而加快开发进度。Q: Unity中Destroy和DestroyImmediate的区别?
答:
两者都是用于销毁Unity对象的方法。如果销毁对象是 Component,则他们都会从 GameObject 移除该组件并将它销毁。如果销毁对象是 GameObject,则会销毁该 GameObject其所有组件以及该 GameObject 的所有变换子项。
实际上,他们的函数参数就不同。
1 | public static void DestroyImmediate (Object obj, bool allowDestroyingAssets= false); |
Destroy:实际的对象销毁操作始终延迟到当前更新循环结束,但始终在渲染前完成。Destroy甚至可以设定销毁对象的延迟时间。
DestroyImmediate:立即销毁对象,不存在延迟。在编辑器下使用时,它可能永久销毁资源。
相关资料:Q: 生成多个GameObject共享材质问题,如何解决?
答:(下面答案存疑)
实例化多GameObject的话,每个GameObject都是独立的,但是他们的材质引用可能指向同一个材质资源,这时候如果使用SharedMaterial函数的话是直接修改原始的资源。这会导致所有指向这个原始材质资源的游戏对象,材质都一起改变。
如果只是修改单个材质的话,那么可以使用Material,显式创建一个材质实例,这时渲染器会重新 创建一份该共享材质的一个唯一副本,但是实例化这个副本的时候会增加Draw Call。除此之外,你还可以使用MaterialPropertyBlock来修改材质属性。Q: 将一个Prefab文件拖入到引擎中,底层发生了什么?
答:(下面答案存疑)
将一个Prefab文件拖入到引擎中,首先Unity会解析这个Prefab文件的序列化数据,接下来Unity通过序列化数据中重新构建Gameobject以及附带的组件,然后再添加所需要的资源引用,最后打上预制体实例化标签。Q: Unity中的生命周期函数被调用的底层原理是什么?
答:(下面答案存疑)
是反射。当脚本被加载时,Unity通过反射来检测脚本中定义了哪些生命周期函数,并为了避免每一帧使用反射,Unity会在加载的时候把函数缓存起来,在主循环中的每一帧的特殊时机去调用对应的生命周期函数。Q: Unity中的Material的作用是什么?
答:
材质球用于定义游戏对象表面的外观。材质球展示了着色器提供的信息,通过改变这些信息来决定最终呈现出来的渲染表现效果。Q: Unity的Shader的作用是什么?
答:
Shader是用来定义3D或2D对象的视觉外观和渲染效果的程序,它在渲染过程中通过每个像素和顶点进行计算,生成最终的图像,它决定了一个物体最终呈现到游戏画面上的效果。Q: Unity中动态批处理和静态批处理的区别?
答:(下面答案存疑)
批处理技术是Unity提供的优化渲染性能的技术,通过减少Draw Call来提高渲染效率,基本原理是合并网格。
动态批处理会将小型动态对象进行批处理,主要消耗的是CPU性能。动态批处理在运行的时候会随时变化,现阶段(Unity5x)动态批处理不支持高清渲染管线。
静态批处理的话只会处理设置为static的对象,主要消耗的是内存。静态批处理结果运行时不变(因为静态对象不能有变化)。静态批处理可支持现阶段Unity官方的所有管线。Unity优化相关
Q: UGUI在active=true或者false(即状态变化的时候) 为何会造成卡顿,怎么优化?
答:(下面答案存疑)
当UI每次active状态变化的时候,Canvas都会重建一次。此时还会造成批处理中断。而界面显示的时候还会触发回调,这个UI下的子物体也跟着重新激活。
优化方案:可以修改UI的透明度或者修改坐标移除画面来优化。Q: 如何优化UGUI?
答:
减少Draw Call的产生,可以使用精灵图集来合并散图,并且确保材质一致。
减少Over Draw,就是避免一个像素被GPU多次渲染,即尽量避免控件叠加,特别是半透明组件之间的互相叠加。
尽量使用TMP,因为TMP的SDF渲染性能优于自带的Text组件。
尽量减少Canvas的重建,避免动态修改Canvas中RectTransform,文本,图片颜色等。
尽量较少布局组件的使用,可以改为手动计算。
关闭不需要事件监听的Raycast Target。
Q: 当一个场景中有大量相同的动态对象,如何优化?
答:
使用GPU实例化。
使用顶点动画贴图。
使用LOD技术。
限制动画计算。
减少模型的网格数量
Q: 执行GameObject.Instantiate时可能出现明显的卡顿,如何解决该问题?
答:
首先我们先使用Unity的性能分析工具来排查,然后分类讨论。
如果是资源加载带来的卡顿,则可以事先实例化即预加载。
如果是脚本组件的话,可以将复杂逻辑提前或者不要放在Awake跟Start函数中。或者只在Start函数中放入必定需要的逻辑,剩下的逻辑使用协程来加载。
对于需要频繁创建的对象,可以使用对象池来优化。
Q: 如何优化物理系统的使用?
答:
如果负担较重的话,可以调整Fixed Timestep。
减少物理计算的对象数量(减少刚体和碰撞体的数量,对于静态的物体可以设为static)。
使用合适的碰撞体,尽量不要使用mesh碰撞,可以使用简单的碰撞体进行拼接。
关闭无关层级的碰撞检测。
按需使用检测模式。
可以分帧进行物理计算,比如射线检测的逻辑等。
Q: 请介绍一些在Unity中提升性能的办法?
答:
动态静态批处理。
GPU实例化。
合并图集。
减少模型顶点数。
使用LOD。
减少光源阴影等。
使用对象池管理。
利用多线程执行复杂操作。
避免过多的Update函数,如果存在空的Update函数要删除。
异步加载。
预加载。
定时主动GC。
Q: 假设一个Unity项目存在帧率不稳定问题,请问你会如何进行优化和排查?
答:
先在Game窗口下的Status中查看其中信息。粗略的判断是CPU负载过高,还是GPU。
如果是CPU负载过高,我们再打开Profiler,查看具体是什么原因导致了CPU负载过高。针对Profiler提供的信息进行优化。
如果是GPU负载过高,我们可以尝试减少Draw Call,减少材质的使用,使用LOD技术,使用GPU Instancing等。





