面试总结1

287 阅读23分钟

点积和叉积的物理意义

  点积:点积结果是个标量,主要用来求两个向量之间的夹角和一个向量在另一个向量上的投影(对于单位向量来讲)。   叉积:叉积结果是个标量,两个向量a,b相乘可以获得一个垂直于a,b的向量c。

Unity组件删除和添加

1. 添加
GameObect.AddComponent<ScriptName>();
2. 删除
Destroy(GameObect.GetComponent<ScriptName>());

Unity3d获取游戏对象的几种方法

点击这里

Unity3d中的碰撞器和触发器的区别

 碰撞器有碰撞的效果,IsTrigger=false,可以调用OnCollisionEnter/Stay/Exit函数; 触发器没有碰撞效果,IsTrigger=true,可以调OnTriggerEnter/Stay/Exit函数。

脚本声明周期

点击这里 个人总结:主要的执行方法

  • Reset:点击Inspector面板中的Reset按钮或者脚本第一次挂载在物体上时执行。
  • Awake:在预制件初始化之后,Start方法运行之前执行(物体在初始化时未被激活,该方法会在其被激活后执行)
  • OnEnable:在对象可用之后被调用,这通常发生在一个脚本的引用被创建时。
  • Start:若对象可用,在第一帧执行前调用。
  • FixUpdate:固定物理时间后调用,可能在一帧内调用多次,其执行时间间隔可通过修改Edit--ProjectSetting--Time面板中的timestep参数来改变。
  • yield WaitForFixUpdate:协程调用。
  • OnTriggerEnter/Stay/Exit:触发器执行的相应方法。
  • OnCollisionEnter/Stay/Exit:碰撞器执行方法
  • OnMouseEvent:鼠标监听执行事件
  • Update:每帧调用一次,是帧更新的主要函数。
  • yield null:协程调用
  • yield WaitForSeconds:协程调用
  • yield www:协程调用
  • yield StartCoroutine:协程调用
  • LateUpdate:所有Update函数调用完执行,通常用在第三人称更新摄像机位置。
  • OnPreCull:在相机剔除场景前被调用。剔除取决于物体在相机中是否可见。OnPreCull仅在剔除执行之前被调用。
  • OnBecameVisible/OnBecameInvisible:当物体在任何相机中可见/不可见时被调用。
  • OnWillRenderObject:如果物体可见,它将为每个摄像机调用一次。
  • OnPreRender: 在相机渲染场景之前被调用。
  • OnRenderObject:在所有固定场景渲染之后被调用。此时你可以使用GL类或者Graphics.DrawMeshNow来画自定义的几何体。
  • OnPostRender: 在相机完成场景的渲染后被调用。
  • OnRenderImage(仅专业版):在场景渲染完成后被调用,用来对屏幕的图像进行后处理。
  • OnGUI:每帧执行多次来回应GUI事件。
  • yield WaitForEndOfFrame:协程调用
  • OnApplicationPause:若暂停被检测到,当前帧执行后就调用此函数,在正常的运行期间调用是有效的。在OnApplicationPause被调用后,额外的用一帧来显示图像表明暂停状态。
  • OnDisabled:对象不可用是执行
  • OnDestroy:这个函数在所有帧更新之后被调用,在对象存在的最后一帧(对象将销毁来响应Object.Destroy或关闭一个场景)。
  • OnApplicationQuit:在应用退出之前所有的游戏对象都会调用这个函数。在编辑器中当用户停止播放时它将被调用。 也就是是说:一帧内执行一次的方法有:Update、协成调用(出WaitForFixUpdate外)、LateUpdate、Rendering方法(如OnRenderImage等等)。

Unity物体之间的通信

主要是运用委托和事件

  • 第一篇
  • 第二篇
  • 第三篇
  • 第四篇 总结:首先要明白:
  • 什么是委托:委托实质上是一个类,这个类实现了可以将方法作为参数进行传递,一个委托可以同时挂载多个方法,只是在运行时这些方法将按顺序全部执行
  • 什么是事件:事件实质上是一个封装了的委托变量,封装了的含义是指:在类的内部,无论定义一个public还是protected类型的事件,其都是private类型的;在类的外部调用是,为其添加和删除的方法的访问修饰符必须与该事件的访问修饰符一样。
  • 理解什么是观察者模式。利用该模式可以实现在物体对象之间传递消息,可以查看第一个链接的相关案例。

什么是面向对象

  • 定义:把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)/泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派(dynamic dispatch)。
  • 三大特性链接

bit和byte的定义及区别

链接 个人总结:

  • bit是计算机存储的单位,在二进制计算机中1bit为0或1,中文为比特,简写b
  • byte是计算机处理数据的单位,lbyte=8bit,中文为张字节,简写B
  • 1kB=1024B=1024*8b

三次握手四次挥手

MipMap作用及计算方法

  • 定义:Mipmap技术有点类似于LOD技术,但是不同的是,LOD针对的是模型资源,而Mipmap针对的纹理贴图资源。使用Mipmap后,贴图会根据摄像机距离的远近,选择使用不同精度的贴图。
  • 缺点:会占用内存,因为mipmap会根据摄像机远近不同而生成对应的八个贴图,所以必然占内存!
  • 优点:会优化显存带宽,用来减少渲染,因为可以根据实际情况,会选择适合的贴图来渲染,距离摄像机越远,显示的贴图像素越低,反之,像素越高!
  • 用途:MipMap可以用于跑酷类游戏,当角色靠近时,贴图清晰显示,否则模糊显示如果我们使用的贴图不需要这样效果的话,就一定要把Generate Mip Maps选项和Read/Write Enabled选项取消勾选!因为Mipmap会十分占内存!
  • 内存大小:Mipmap 会多占 1/3 内存。
  • 多占内存原因:想要用数学求解,必须先了解 Mipmap 各个级别的尺寸大小是如何确定的,正常来说,Mipmap 会生成很多级别,每一级别的宽高都是上一个级别的 1/2,也就是说面积为上一个级别的 1/4,直到最终只有 1x1 分辨率的大小。 举个例子,一张 1024 x1024 的纹理,生成 Mipmap 后,会新产生 512 x 512,256 x256,128 x 128,64 x 64,32 x 32,16 x 16,8 x 8,4 x 4,2 x 2,1 x 1 这几张不同纹理级别的纹理。可以手动计算一下,新产生的纹理大小总和是 349525 个像素,而原来的 1024 x1024 纹理有 1048576 个像素,349525 / 1048576 约等于 0.33333302,也就是大约 1/3。
  • 推理链接

C#常用容器及其数据结构

  • Array: 属于System命名空间。数组,在内存中是连续存储的,所以它的索引速度非常快,但是插入删除比较慢。

  • ArrayList:是命名空间System.Collections下的一部分,在使用该类时必须进行引用,同时继承了IList接口,提供了数据存储和检索。ArrayList对象的大小是按照其中存储的数据来动态扩充与收缩的。所以,在声明ArrayList对象时并不需要指定它的长度。ArrayList会把所有插入其中的数据当作为object类型来处理,在我们使用ArrayList处理数据时,存在装箱和拆箱操作,会损失性能。其底层数据结构使用的是object类型数组。类似于C++中的Vector,类似于java中ArrayList和Vector

  • List“T”:包含在System.Collections.Generic命名空间中,底层数据结构就是数组,但是存取数据避免了拆箱和装箱操作,类似于C++里面的Vector“T”。

  • LinkedList:包含在 System.Collections.Generic命名空间中,双向链表,LinkedList“T”提供 LinkedListNode“T” 类型的单独节点,因此插入和移除的运算复杂度为 O(1)。可以移除节点,然后在同一列表或其他列表中重新插入它们,这样在堆中便不会分配额外的对象。由于该列表还维护内部计数,因此获取 Count 属性的运算复杂度为 O(1)。LinkedList“T” 对象中的每个节点都属于 LinkedListNode“T” 类型。由于 LinkedList“T” 是双向链表,因此每个节点向前指向 Next 节点,向后指向 Previous 节点。类似于java中的LinkedList

  • HashSet:属于System.Collections.Generic命名空间。HashSet“T”类提供了高性能的集运算。一组是一个集合,不包含任何重复的元素,且的元素顺序不分先后。用了hash table来储存数据,是为了用O(n)的space来换取O(n)的时间,也就是查找元素的时间是O(1)。底层使用的是哈希表,类似于java中的Set“T”。

  • HashTable:Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似key/value的键值对,其中key通常可用来快速查找,同时key是区分大小写;value用于存储对应于key的值。Hashtable中key/value键值对均为object类型,所以Hashtable可以支持任何类型的key/value键值对。所以用的存取的时候存在装箱和拆箱操作。底层使用的是哈希表,类似于java中的HashMap,HashTable

  • Dictionary:包含在System.Collections.Generic命名空间下。也是键值容器,存入对象是需要与[key]值一一对应的存入该泛型。相对于HashTable,存取时不需要拆箱和装箱出走。底层使用的是哈希表

  • java中改的TreeMap、TreeSet和c++中的hashMap为红黑树

**总结:**C#所有泛型容器都在System.Collections.Generic命名空间下,非泛型容器在System.Collections空间下。

数组和链表内存上的区别

  • 数组是连续内存,索引查找比较快,添加删除比较慢,因为需要移动后面的元素
  • 链表是不连续内存,添加删除比较快,查找比较慢,因为需要从头查找

Java和C#泛型区别

C#泛型无论实在源码中、编译后的IL文件中(Intermediate Language,中间语言,这时候泛型是一个占位符),还是在运行期的CLR中都是切实存在的,也就是List与List就是两个不同的类 型。而Java泛型只是在源码中存在,在编译后的字节码中,就已经被替换为了原始类型,并在相应地方插入了强制类型转换,因此对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类。所以C#是真泛型,Java是假泛型。

C#运行流程

(1)C#编译器先将源代码编译成IL文件和元数据,并连同其他资源文件合并成程序集,程序集的可执行文件存储在磁盘上,通常具有的扩展名为 .exe 或 .dll(编译阶段)。 (2)程序集合并完成后,若程序集可执行,在Main()方法执行之前,window开启一个进程,并再加载MSCOREE.DLL,然后进程的主线程会调用MSCOREE.DLL中的方法初始化CLR(公共语言运行库),CLR中JIT(即时编译)会把IL语言转换为cpu指令(也称及其指令),并以文件方式存储在硬盘上,操作系统将文件从硬盘读入内存,CPU从内从取出指令执行(运行阶段)。 延伸: (1)开发者编写的代码编译后,不依赖于操作系统和特定的CPU架构机器指令,而是依赖于一种中间的,在各个操作系统上都能执行的代码,这种代码Java里面叫做ByteCode(字节码),.NET里面我们称之为MSIL指令(微软中间语言)。 (2)不管是Java的字节码还是.NET的MSIL指令仍然需要最终被翻译成CPU能够执行的机器指令。这个功能是由一个运行在特定操作系统上的软件来完成,这个软件我们称之为“虚拟机”。

C#内存结构

C#主要由内存主要分为内存主要分为三类:

  • 栈内存:主要用于存储值类型数据
  • SOH堆: 存储小的引用对象
  • LOH堆:存储大对象,主要是大小大于85000字节的对象

C#中GC运行时间

个人总结:

  • 托管堆:CLR初始化后,会保留一块连续内存,这就是托管堆。
  • 简要流程:C#编译器遇到new指令时,会在IL中添加一条newobj指令,CLR遇到该指令会计算该对象的大小,然后计算堆中是否存在该大小的内存,若存在,则直接分配相应大小的内存,并调用对象的构造函数将对象放在这块内存中,若不存在则需要执行相应的垃圾回收。
  • 垃圾回收时,垃圾回收器会首先认为堆中的所有对象都是垃圾,然后挨个遍历应用程序根列表(需要单独介绍应用程序根,列表由JIT编译器和CLR编译器进行维护,垃圾回收器可以访问这些对象)和引用对象,并构建一个由应用程序根和其所指向对象构成的图,由此可判断哪些对象是可达的,可达的话,就会标记此对象,若此对象已被标记,则不会再对其进行标记。(也就是根据这些应用程根,查看根所指向的内存地址,这块地址中存储的对象就是可达的,还会被引用,所以会被标记)。
  • 标记完毕后,垃圾回收器会遍历堆中的对象,未被标记的对象则会被当成垃圾释放其内存。并将不是垃圾的对象移到一起,消除内存碎片。
  • 注意,在移动可达对象之后,所有引用这些对象的变量将无效,接着垃圾回收器要重新遍历应用程序的所有根来修改它们的引用。在这个过程中如果各个线程正在执行,很可能导致变量引用到无效的对象地址,所以整个进程的正在执行托管代码的线程是被挂起的。 应用程序根:
  • 全局对象和静态对象的引用
  • 应用程序代码库中局部对象的引用
  • 传递进一个方法的对象参数的引用
  • 等待被终结(finalize)对象的引用
  • 任何引用对象的CPU寄存器 垃圾回收在下列情况下发生:
  1. 申请的空间超过0代内存大小或者大对象堆的阀值,多数的托管堆垃圾回收在这种情况下发生
  2. 在程序代码中调用GC.Collect方法时;如果在调用GC.Collect方法是传入GC.MaxGeneration参数时,会执行所有代对象的垃圾回收,包括大对象堆的垃圾回收
  3. 操作系统内存不足时,当应用程序收到操作系统发出的高内存通知时
  4. 如果垃圾回收算法认为做二代回收是有收效时会触发二代垃圾回收
  5. 每一代对象堆的都有一个所占空间大小阀值的属性,当你分配对象到某一代,你增长了内存总量接近了该代的阀值,或者分配对象导致这一代的堆大小超过了堆阀值,就会发生一次垃圾回收。因此当你分配小对象或者大对象时,会对应消耗0代堆或者大对象堆的阀值。当垃圾回收器将对象代数提升到1代或者2代时,会消耗1、2代的阀值。在程序运行中这些阀值是动态变化的。(动态变化,也就是垃圾回收过程中,0,1,2代垃圾的可使用内存不断变化) 垃圾回收是分代的:
  • 0代:从没有被标记为回收的新分配的对象
  • 1代:在上一次垃圾回收中没有被回收的对象
  • 2代:在一次以上的垃圾回收后仍然没有被回收的对象
  • 每代垃圾存储超过其最大值都会触发垃圾回收。
  • 第0,1代大约16M左右,第二代是根据应用程序不断变化的,可达几GB。 链接: 重点一 重点二

垃圾回收方式

  • 托管对象会被CLR自动回收,如分配在堆中的对象和分配在栈中的变量
  • 通过调用GC.Collect()方法,强制垃圾回收,不推荐使用
  • 通过析构函数,即finalize函数。
  • 实现IDispose接口
  • 析构函数和IDispose接口共同使用

渲染流程

主要分为三个大阶段:应用阶段,几何阶段,光栅化阶段。

应用阶段

  • 将资源数据加载到显存:所渲染的资源数据需要从硬盘加载到内存,在加载到显存(这些数据包括顶点位置信息,法线信息,顶点颜色,纹理坐标等),因为大部分显卡对显存访问快,而对内存没有访问权限(例如PC,显存和内存分开的)。
  • 设置渲染状态:规定了场景中的网格是如何渲染的,例如使用那个着色器,使用那个材质等等,如果不更改渲染状态,则所有网格都使用同一种渲染状态。 这一小阶段比较耗时,尤其是渲染状态之间的切换(unity中可以简单理解为切换材质球)。
  • 调用DrawCall:也就是CPU向GPU发送渲染命令,告诉GPU绘制几何物体。

几何阶段

  • 顶点着色器:这一阶段的处理单位是顶点,每输入进一个顶点,就会运行一次顶点着色器。顶点着色器本身不会产生和删除顶点。顶点着色器可以进行坐标变换(必须要做的工作就是把顶点坐标从物体空间变换到其次裁剪),顶点光照,输出其他阶段后续所需的数据等。
  • 曲面细分着色器:细分图元,并不了解。
  • 几何着色器:可以生成新的顶点,或执行图元的着色操作等,做unity手游就放弃把,metal不支持。

几何着色器和曲面细分着色器都是可选的,不一定非要设置。

  • 裁剪:几何体和视锥体有三种关系:完全在视野内,部分在视野内,完全在视野外。完全在视野内的直接传递到下一阶段,完全在视野外的不会传递,部分在视野外的则需要裁剪。会在剪裁平面和几何体的交界处产生新的顶点。
  • 图元装配:也就是将裁剪过后的几何体重新组装成图元。如下图ABC在被裁剪后,会生成ABD和BDE两个三角形
  • 屏幕映射:将顶点x和y坐标转换成屏幕坐标。这个阶段得到的坐标决定了这个顶点对应那个像素以及距离这个像素有多远(z值,也就是深度)。

光栅化阶段

  • 三角形遍历:这个阶段判断那些像素所在的位置被三角形网格所覆盖 ,如果某个像素位置被覆盖,则会在这个位置生成一个片元。这个片元的相关信息(如颜色,uv坐标,深度值,法线等)都是有三角形的三个顶点根据某个算法插值得到的。例如软渲染种常用的扫描线算法
  • 片元着色器:每个片元都会执行一次,也可以一个物体覆盖了多少像素,就会执行多少次。这一阶段可以完成很多工作,例如:逐像素光照,纹理采样,Alpha裁剪等等。
  • 逐片元操作:主要包含三项:模板测试,深度测试,颜色混合等等。这三项操作我们可以自己决定是否开启。

结构体和类的区别

值类型和引用类型区别

重载和重写区别

  • 重载是对于同一类中的方法来讲:有相同的方法名,相同的返回值,只是参数不同
  • 重写是对继承来说,子类可以重写父类中的方法。要求方法名,返回值,参数都必须相同。

协成和线程

  • 协成:在主线程运行时同时开启另一段逻辑处理,来协助当前程序的执行。换句话说,开启协程就是开启一个可以与程序并行的逻辑。可以用来控制运动、序列以及对象的行为。
  • 协成同一时间只可以有一个运行,协成还是在主线程中运行(从脚本执行流程中可以找到),线程可以并发执行,

线程和进程

yield返回值

  • WaitForEndOfFrame,顾名思义是在等到本帧的帧末进行在进行处理,这个问题不大,比较不容易搞错
  • yield return null/任何数字:暂缓一帧,协程在下一帧的所有Update函数调用后继续执行。
  • yield return new WaitForSeconds:在一个指定的时间延迟后继续执行,在所有的Update函数被调用之后。(这个要注意的是1·实际时间等于给定的时间乘以Time.timeScale的值。2·触发间隔一定大等于1中计算出的实际时间,而且误差的大小取决于帧率,因为它是在每帧处理协程的时候去计算时间间隔是否满足条件,如果满足则继续执行。例如,当帧率为5的情况下,一帧的时间为200ms,这时即使时间参数再小,最快也要200ms之后才能继续执行剩余部分。)
  • yield WaitForFixedUpdate():在所有脚本上所有的FixedUpdate被调用之后继续执行。
  • yield WWW:在WWW加载完成之后继续执行。
  • yield StartCoroutine(MyFunc):用于链接协程,此将等待MyFunc协程完成先。

foreach实现

点击这里 个人总结:首先要实现IEnumerable接口,并实现里面的GetEnumerator方法,GetEnumerator方法返回的是一个实例化的IEnumerator遍历器,IEnumerator中包含两个属性。

  • Current,就是返回这个遍历工具所指向的那个容器的当前的元素。
  • MoveNext 方法就是指向下一个元素,当遍历到最后没有元素时,返回一个false。

所以还需要一个实现了IEnumerator接口的类。 (或者在GetEnumerator方法中使用yield return)。

private、protected、public、internal区别

  • private是完全私有的,只有在类自己里面可以调用,在类的外部和子类都不能调用,子类也不能继承父类的private的属性和方法
  • protected虽然可以被外界看到,但外界却不能调用,只有自己及自己的子类可以调用(protected的属性和方法都可以被子类所继承和调用)。
  • public对任何类和成员都完全公开,无限制访问
  • internal同一应用程序集内部(在VS.NET中的一个项目中,这里的项目是指单独的项目,而不是整个解决方案)可以访问。

接口和类的区别

  • 抽象类可以有构造方法,接口中不能有构造方法。

  • 抽象类中可以有普通成员变量,接口中没有普通成员变量

  • 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

  • 抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。

  • 抽象类中可以包含静态方法,接口中不能包含静态方法

  • 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

  • 一个类可以实现多个接口,但只能继承一个抽象类。

  • 接口可以用于支持回调,而继承并不具备这个特点.

  • 抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然您也可以声明为虚的.

Lua和C#交互

  • ToLua在初始化的时候会调用生成Warp文件的Register方法,将每个类型通过tolua_beginclass注册到lua的注册表中(每个类型都是单独的表),并返回该类型在注册表中的id,然后调用tolua_function将Wrap文件中的生成方法注册到这个单独的表中。
  • lua获取C#对象的时候,首先获取该类型在lua的注册表中的meta_id,然后将这个C#对象保存在ObjectTranslator类中的objects(其实就是个list)变量中(lua访问该对象的时候,只需要传回这个索引,就能找到该对象),最后将C#对象在list的索引和meta_id作为参数,调用tolua_pushnewudata方法,这个方法会生成一个userdata,保存索引,并在全局注册表中查找id为meta_id的表,作为userdata的元表,这也是为什么lua调用c#对象的成员方法,最终会执行到Warp文件中。
  • lua调用C#方法,首先会将栈中的userdata转化为C#对象objects(其实就是个list)变量中的的索引,然后获取C#对象,最后操作C#对象执行某些方法,最后将结果放入栈中,返回给lua。
  • Lua将方法注入C#,会调用toluaL_ref方法将该方法放入lua注册表(不然对于匿名函数来讲,没有其他引用,会被gc回收),并返回该方法在lua注册表的id(包装在LuaBaseRef类中),C#调用该id获取lua方法,并调用。

lua 优化

  • 多使用局部变量
  • 少用字符串连接
  • 可以预设置table大小,减少rehash
  • C#从lua端获取的对象,不再需要的时候要及时主动调用Dispose()方法,主动解除Lua 端对象的引用
  • lua端缓存的一些C# 的userdata,在不需要使用的时候也及时置为nil,从而调起__gc 元方法,清理C#端ObjectPool缓存,进而释放C#内存
  • 若一个MonoBehaviour对象缓存在这个Pool里,当它被Destroy之后,Mono对象的引用仍旧被缓存在Pool里,若没有及时从Pool中清除引用的话将造成GC无法回收这个Mono对象,进而导致被这个Mono对象引用的一片内存都无法得到及时释放,从而导致内存泄漏。
  • 少用gameobject.transform.position 这样方法,替换为static方法SetPos(go,x,y,z).少了一次从ObjectTranslator类中转换对象,少了一次缓存(C#这边保存tranform的id)
  • 多传递int,float,double类型的值,少传递复杂class类型的值,因为复杂类型每次传递都需要从index转为C#对象(dictionary查询),简单类型拿出来就可以用。

Unity其他相关面试题

100道

线程池相关

共七篇