独立开发近一年游戏没什么成果,最近开始找工作,今天面试了第一家公司,结果很糟糕,在这里记录反省。

公司位置在浦东世纪大道附近,约的 10 点面试,提早了一个小时到,就在楼下等到快 10 点再上去的。

面试过程没有笔试,面试官拿着一张面试题直接问我,很多题目没答好,有些是因为紧张,有些是真的不太了解。面试官最后直接说 “今天就这样吧,你 C# 基础太差”(不是原话,但是差不多)。以下为回顾的面试题

一、说说 lua 的一个元表

现在的理解是内置一个数组和一个字典的容器,可以用于实现继承。

二、手写一个栈

第一份工作有用过 lua,所以面试官上来就叫我用 lua 手写一个栈,我直接说大部分语法忘记了,面试官有点不耐烦地说那用 C# 写一个。这时候我还是没太反应过来是要我实现一个栈,我居然还以为他只是要我新建一个 Stack,还问他要什么类型的,还是不耐烦地回答我说 “int吧”。我觉得不对劲,再问了一遍才明白是要我实现一个栈功能的类,这个时候他应该以为我不知道栈,还强调了一遍后进先出。现在才刚刚想起来我当时的确弄混了栈和队列,写的是个队列。

出于紧张,一时间没有思路,问了一句能不能用其他数据结构,答不能,我懵了。面试官这个时候已经不想继续了,直接说 “不然今天就这样吧”。我再问一遍之后跟我说可以用数组,实际上这里是给我挖了一个坑。然后我就开始动手写,写的是个队列(狗头,现在回想真的太蠢了。不过面试官可能也没有看得很仔细,没有指出我实现的不是栈而是队列,但是指出了其他问题。

代码就不贴了,说说面试官讲的问题

1.用一个成员变量作为最后入队的下标,面试官说意义不明,而且没有初始化。意义不明是因为栈不需要队尾标记,初始化怪自己没认真检查。

2.出队的时候下标超出数组范围没有给出异常抛出。这个一方面是不应该用数组,另一方面我得好好看看这种异常情况除了打印以外,抛出要怎么写。

查阅后知道可以用链表实现链栈,用数组实现顺序栈,自己再撸一遍 C# 贴出来,先是链栈:


public class MyStack<T>
{public int Count{get { return count; }}private int count;private MyNode<T> _myNode;public void Clear(){_myNode = null;count = 0;}public bool Contains(T val){MyNode<T> checkNode = _myNode;while (checkNode != null){if (checkNode.val.Equals(val)) return true;checkNode = checkNode.lastNode;}return false;}public T Peek(){if (_myNode != null) return _myNode.val;throw new InvalidOperationException("Stack is empty.");}public T Pop(){if (_myNode != null){T val = _myNode.val;_myNode = _myNode.lastNode;count--;return val;}throw new InvalidOperationException("Stack is empty.");}public void Push(T val){MyNode<T> newNode = new MyNode<T>(val);newNode.lastNode = _myNode;_myNode = newNode;count++;}public T[] ToArray(){T[] arr = new T[count];MyNode<T> checkNode = _myNode;for (int i = 0; i < count; i++){arr[i] = checkNode.val;checkNode = checkNode.lastNode;}return arr;}private class MyNode<T>{public T val;public MyNode<T> lastNode;public MyNode(T val){this.val = val;}}
}

顺序栈:

class MyStack<T>
{private T[] _stackArr;private int _index;public int Count{get { return _index + 1; }}public MyStack2(){_stackArr = new T[8];_index = -1;}public void Clear(){_index = -1;}public bool Contains(T val){for (int i = 0; i < _index; i++){if (_stackArr[i].Equals(val)) return true;}return false;}public T Peek(){if (_index == -1) throw new InvalidOperationException("Stack is empty.");return _stackArr[_index];}public T Pop(){if (_index == -1) throw new InvalidOperationException("Stack is empty.");return _stackArr[_index--];}public void Push(T val){if (Count >= _stackArr.Length){T[] temp = new T[_stackArr.Length * 2];for (int i = 0; i < _stackArr.Length; i++){temp[i] = _stackArr[i];}_stackArr = temp;}_index++;_stackArr[_index] = val;}public T[] ToArray(){T[] ret = new T[Count];for (int i = 0; i < Count; i++){ret[i] = _stackArr[i];}return ret;}
}

晚些再用 lua 撸一遍。

三、DrawCall 是什么?如何减少 DrawCall?如何减少在 ui 中的 DrawCall 使用?顶点的坐标空间变换过程?

这个算是面试官随口问的,因为我简历上没写渲染相关。

第一个问题我随便回答的绘制过程。回头翻看了冯乐乐的《Unity Shader入门精要》,总结一下应该回答: CPU 对 GPU 发起的对一个图元列表的命令。

第二个问题上网搜了一圈,感觉 陈嘉栋(慕容小匹夫)大佬的文章 深入浅出聊优化:从Draw Calls到GC讲的比较详细。这里要注意一下,DrawCall 的优化实际是对 CPU 的优化。稍微总结一下方法以防以后再遇到类似问题:

1、静态批处理:使用相同材质,并且不需要移动的物体,都在 Inspector 右上角勾选 Static 的下拉菜单勾选 Batching Static 之后完成

静态批处理前:

静态批处理后:

2.动态批处理:材质、网格相同的两个物体 Unity 会自动进行批处理,在 Unity5 之前有对 Scale 限制,Unity5 之后没有,但是还有如下限制:

  • 网格物体的顶点需要小于 900;
  • 如果着色器使用了顶点位置、法线和 UV 值(纹理坐标)三种属性,只能批处理 300 顶点一下的物体,如果使用了更多的属性,最大顶点数量只能是 900 / n(n 为使用的属性数量);
  • 多通道(多 pass)的 Shader 会妨碍批处理操作;
  • 带有 lightmap(光照纹理)的物体需要指向 lightmap 的同一位置。

另外还需要在 Edit —> Project Settings —> Player —> Other Settings 中勾选 Dynamic Batching(Unity 2019.4.2f1)。

开启动态批处理前,左:相同的两个物体;右:不同的两个物体:

                                

开启动态批处理后,左:相同的两个物体;右:不同的两个物体:

                              

3.通过把纹理打包成图集来尽量减少材质的使用,具体操作看 weixin_44819220 大佬的文章 Unity3d 打包图集。

4.尽量少用反光、阴影一类效果,因为会使物体多次渲染。

第三个问题在 怣*痛 大佬的文章 UI优化 drawcall的优化 感觉整理较好,还是自己列举一下以防再次碰到:

1.减少 mask 的使用,每个 mask 会单独计算 DrawCall,可以考虑用带通道的图片代替 mask 的遮罩功能。

2.合并图集,DrawCall 会按照图集逐个进行批处理。

3.在一串层级关系中,带有来自相同图集图片的组件最好是父子层级关系,即两个组件之间没有其他层级插入,否则还是会进行多次批处理。

4.减少 UI 层级的深度。

5.常用的部分和不常用的部分分在两个节点下,动静分部。

7.尽可能去除组件的 Raycast Target(射线检测)属性。(效果不明显)

第四个问题我好像是答对了,具体还是参照 冯乐乐的《Unity Shader入门精要》,顺序是 模型空间—>世界空间—>观察空间—>裁剪空间(齐次裁剪空间)。

四、如何优化内存

在这个问题之前,面试官先问了对象池能用在什么地方,我就回答反复使用的物体,之后问怎么优化内存我就先答了刚刚说的对象池,然后含糊地说了减少模型顶点,string 改用 stringbuilder,答得很不完整。

看了 UWA 创始人张鑫巨佬的文章  性能优化,进无止境-内存篇(上)和 性能优化,进无止境---内存篇(下)。以下总结一下要点:

1.资源内存占用:

(1) 纹理:

A.纹理格式:根据硬件选择合适的纹理格式;

B.纹理尺寸:尽可能降低纹理尺寸;

C.UI资源的Mipmap功能:在UI纹理中开启Mipmp功能不会提升渲染效率,反而增加无用的内存占用,建议关闭;

D.Read & Write:开启纹理的 “Read & Write” 会使纹理内存增大一倍。

(2)网格:如果没有 Color、Normal、Tangent 等属性的 Mesh 和有这些属性的 Mesh 进行合并,没有这些属性的 Mesh 也将会被添加上这些属性,造成很大的内存开销。

2.引擎模块自身占用:较多开销在 WebStream 和 SerializeFile,绝大部分内存分配是由 AssetBundle 加载资源导致,要及时释放 AssetBundle 或者将解压后的 AssetBundle 数据存储于本地。

3.托管堆内存占用:Mono 不会把堆内存返还给系统。

4.内存泄漏:资源泄露,切换场景没有释放资源。

5.资源冗余:

(1)同样的资源被打入两个 AB 包;

(2)直接预制不同的 Material,而不是修改同一个 Material 的属性,因为修改属性会实例化一个新的 Material。

另外把网上到处都在转的面试题中优化内存部分也写在这里:

1.压缩自带类库;

2.将暂时不用的以后还需要使用的物体隐藏起来而不是直接 Destroy 掉(对象池);

3.降低模型的片面数、降低模型的骨骼数量、降低贴图的大小;

4.使用光照贴图,使用多层次细节(LOD),使用着色器(Shader),使用预设(Prefab)。

五、GC的底层原理

这个我完全没有答上来。

看了 carsonche 大佬的文章 Unity GC垃圾回收 和 zblade 大佬的文章 Unity优化之GC——合理优化Unity的GC,两位似乎都是翻译自官方文档,描述得比较简单一些。因为想强化记忆,所以在这里手打一遍大致内容。

简介:回收不再使用的内存空间。

原理:Unity 采用 Boehm GC(2019 之后加入 增量式垃圾回收),其使用 Mark-Sweep(标记-清除),即通过将使用中的内存标记后遍历清除未标记内存。具体标记过程依赖一个根对象遍历所有内存,标记结束后遍历整个堆内存清除未标记内存。GC 在堆(heap)中操作,堆栈栈(stack)中存储较小、生命周期较短的数据,堆与之相反,所以如果没有 GC 的话堆会一直增长。

堆栈的运行方式类似数据结构的 Stack,操作十分快捷简单,就不多做阐述。

堆的内存分配过程:

  1. 检测内存空间是否足够,如果有则直接分配,否则进入 2;
  2. 触发 GC 回收废弃的内存空间,再检测内存空间是否足够,如果有则分配,否则进入 3;
  3. 扩展堆内存的大小,分配内存空间。

GC 的具体操作(操作耗费大,堆上使用中的空间越多耗费越大):

  1. 通过根对象检查堆内存上所有存储变量;
  2. 检测变量的引用是否在激活状态;
  3. 把引用不在激活状态的变量,标记为可回收;
  4. 移除可回收的变量,回收对应的内存空间;

GC 触发时机:

  1. 上文的内存分配过程中的触发(所以频繁分配堆内存的话 GC 会被反复触发);
  2. 根据不同平台,以一定的频率自动触发;
  3. 手动强制执行回收。

优化:

  1. 尽量避免在 Update 或碰撞检测等函数中创建引用对象,可以用成员变量缓存反复使用;
  2. 数据结构的重置尽量使用清空,而不是重置;
  3. 使用对象池,避免反复创建和销毁需要反复使用的对象;
  4. string 每次操作都会分配内存,如果字符串有一部分经常改变,另一部分不会的话,把两个部分分开;
  5. 也可以用 StringBuilder 代替 String;
  6. 尽量介绍 Debug.Log() 等函数,它无论如何会产生一个字符串;
  7. 许多函数可以代替属性或者其他函数,可以避免这些属性或者函数返回时分配新的内存;
  8. 减少装箱操作,装箱操作会在堆内存分配一个 System.Object 类型的引用;
  9. 协程 yield return 返回参数时会产生不必要的内存垃圾,如果返回 null 则不会
  10. 协程如果要返回一个 new 对象的话,可以用缓存优化;
  11. 可以用其他方法代替协程,比如计时器可以写在 Update 中代替协程。
  12. Unity 5.5 之前的 foreach 需要在堆内存产生一个 System.Object ,尽量用 for 或者 while 代替;
  13. 减少函数引用(这个如果引用不是特别频繁的话,我觉得没有必要)。

六、协程和线程的区别

面试常见题了,凭着印象说:线程可以开起多条,协程任意指定时间只能有一个在运行(因为都在主线程当中),主线程以外的线程无法访问 Unity3D 的相关对象、方法、组件,线程安全需要加锁。面试官没说什么,我回答应该是不够的。

网上没有找到很满意的相关文章,之后又找到的话再放上来。

七、字典和列表的时间复杂度

面试官开始直接问我字典的时间复杂度是多少,我没反应过来,随口答了 O(nlogn),显然不对,只能说不知道,补了一句我只知道字典是用哈希表实现的。面试官又问了列表的时间复杂度是多少,我还是答不知道。之后面试官就叫我走了。

主要还是出于紧张,当时一直脑子一团乱没明白问的到底是啥,查阅后知道这个问题要答对数据结构进行添加、查找、Contains 等的时间复杂度,如果当时反应过来是回答这些的话,想一下就能想出来了。

字典 Dictionary:1.获取的时间复杂度为 O(1),因为使用哈希表索引。2.添加的时间复杂度再不需要扩容时为 O(1),需要扩容时为 O(n)。3.ContainsKey 的时间复杂度为 O(1),因为使用哈希表索引。4.ContainsValue 的时间复杂度为 O(n),因为需要遍历整个字典查找。

列表 List:1.获取的时间复杂度为 O(1),直接用整型下标索引获取得到。2.添加的时间复杂度同字典。3.Contains 的时间复杂度为 O(n),因为需要遍历列表查找。

2020-08-21 第一次面试小结相关推荐

  1. 《惢客创业日记》2020.08.21(周五)天使投资人的底限

    今天上午,在凉粉儿的公司,与惢客的两位天使投资人"凉粉儿"和"汽水"做了一次很重要的沟通,虽然有了近两年的合作,但是,真正三个人坐在一起沟通惢客,还是头一回.这 ...

  2. 2020年4月2日华为消费者BG部门Android安卓第一次面试经验(一面,面经,编程代码题)

    一.问题背景 博主于2020年4月7日参加了腾讯Android安卓第一次面试,以下为华为面试官当场让博主在15分钟内必须实现的编程代码题目. 二.2020年4月2日华为消费者BG部门Android安卓 ...

  3. 2020人工神经网络第一次作业

    ➤ 01第一题 1.问题描述 异或问题是一类简单的非线性可分问题,通过人工神经网络完成对异或逻辑关系的实现,可以帮助加深对人工神经网络算法的理解. ▲ 异或问题两种表示 ▲ 异或问题两种表示图示 相关 ...

  4. (十三:2020.08.28)CVPR 2015 追踪之论文纲要(译)

    CVPR 2020 追踪之论文纲要(修正于2020.08.27) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...

  5. (十一:2020.08.28)CVPR 2017 追踪之论文纲要(译)

    CVPR 2017 追踪之论文纲要(修正于2020.08.28) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...

  6. 关于第一次面试总结(嵌入式软件开发工程师)

    第一次面试总结 首先,笔试: 一.问死锁是什么,死锁的原因有哪些?死锁的四个必要条件是神马?如何解开死锁? 死锁: 指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力 ...

  7. (十四:2020.08.28)CVPR 2014 追踪之论文纲要(译)

    CVPR 2020 追踪之论文纲要(修正于2020.08.28) 讲在前面 论文目录 讲在前面 论坛很多博客都对论文做了总结和分类,但就医学领域而言,对这些论文的筛选信息显然需要更加精细的把控,所以自 ...

  8. 2020人工神经网络第一次作业-参考答案第九部分

    本文是 2020人工神经网络第一次作业 的参考答案第九部分 ➤09 第九题参考答案 1.数据整理 根据char7data.txt中的文件将训练样本(21个字符)以及对应的输出值转化到两个矩阵:char ...

  9. 2020人工神经网络第一次作业-参考答案第八部分

    本文是 2020人工神经网络第一次作业 的参考答案第八部分 ➤08 第八题参考答案 1.题目分析 (1) 数据下载 从https://www.cosy.sbg.ac.at/~pmeerw/Waterm ...

  10. 记录大三第一次面试经历

    今天面试了一家不大的公司的java实习生,面试流程很简单,要求也不高,但是呢,由于之前没有进行过真正的面试,积累的经验比较少,该准备的没准备,导致结果很不好.经过一个好朋友的指点,找到了一些失误点,所 ...

最新文章

  1. mysql innodb表分区
  2. TiDB 源码阅读系列文章(十八)tikv-client(上)
  3. java扫描包下类_实现获取扫描指定包路径下的jar文件或class文件JarPojoHandler类示例代码...
  4. RichTextBox 右键显示 ContextMenuTrip
  5. 排序算法以及基本数据结构
  6. python数字类型floatcomplexint_浅谈python 四种数值类型(int,long,float,complex)
  7. c语言指针跨函数使用内存
  8. STM8S103FP6芯片PB_4/PB_5输出问题
  9. 【Unity3D日常BUG】Unity3D 中听不到声音解决方案
  10. c语言oj查重,GitHub - shawnsky/hshe: Online Judge System 在线评测系统 代码查重 作业质量...
  11. Intellij IDEA创建Scala项目
  12. [导入]MPQ 文件系统完成
  13. 错误-The server encountered an unexpected condition that prevented it from fulfilling the request
  14. 实现元素水平垂直居中的4种方法
  15. [NodeJS] Mongoose Populate 基本使用
  16. java实现海盗比酒量
  17. 等级测评——定级、等级划分及测评时间
  18. 「Python条件结构」使用if结构实现密码验证
  19. echarts dataView数据对齐及表格复制
  20. 统信uos安装粤政易

热门文章

  1. 网页数据抓取工具 (谷歌插件 web Scraper)
  2. 百度首页被tn劫持的办法有那些、两种解决百度劫持的方法
  3. Linux vim编辑器在哪,【Linux】Linux中VIM编辑器的使用
  4. 基于热传导方程的高温作业专用服装设计(三)
  5. 【我的OpenGL学习进阶之旅】Assimp库支持哪些3D模型格式?
  6. 《Spring Boot极简教程》第9章 Spring Boot集成Scala混合Java开发
  7. java编程过程——流程图
  8. ActiveReportsJS 2.2.1 中文特殊版
  9. java如何看手机型号,基于JAVA代码 获取手机基本信息(本机号码,SDK版本,系统版本,手机型号)...
  10. java实现人脸识别源码【含测试效果图】——前期准备工作及访问提示