2018年秋天,我参加了商汤科技校招iOS开发工程师的第一场笔试。当时商汤在AI视觉领域风头正盛,投递这个岗位的人非常多,笔试作为第一道筛选关卡,题量不小、覆盖面也很杂。考完之后我第一时间把题目和考点记录了下来,后来也陆续把这些内容分享给了学弟学妹。今天整理成一篇完整的复盘笔记,希望能给准备iOS校招、尤其是准备投AI公司iOS岗位的朋友一些参考。
这篇文章不是官方真题答案,而是我从考生视角做的考点还原和思路拆解。内容包括笔试整体结构、iOS基础考点逐一分析、编程题解题复盘、AI公司特有的考察方向,以及一些非常实用的避坑经验。适合正在准备iOS校招笔试的人、想了解AI公司技术岗位考察风格的人,以及想系统梳理iOS知识框架的开发者参考。
1. 笔试整体结构与出题逻辑
1.1 试卷总体布局
商汤这场笔试是线上笔试,考试时间一共90分钟,题量大概在40道左右。题型分布大致为:单选题约20道、多选题约10道、编程题2道、问答题1道。选择题覆盖了iOS开发基础、计算机基础、算法与数据结构、机器学习常识四个方向,编程题以算法为主,问答题则偏向系统设计。
从题型配比能看出一个明显特征:这不是一场纯粹的iOS语法考试,而是一场“基本功+方向匹配度”的综合筛选。商汤作为AI公司,对iOS工程师的定位不是简单的界面开发,而是能做算法集成、性能调优、底层优化的客户端工程师,所以笔试里计算机基础的比例明显高于一般互联网公司的同类岗位。
1.2 考察侧重点分析
我复盘后发现,这套笔试的出题逻辑其实分三层:
第一层是硬性门槛,也就是iOS基础知识,主要筛选“能不能干活”。这一部分题不算难,但覆盖面很广,从内存管理到多线程、从Runtime到网络请求,基本把iOS日常开发的高频知识点都扫了一遍,没有实际写过项目的人很容易栽在这里。
第二层是算法与数据结构,主要考察“能不能解决复杂问题”。商汤的算法笔试难度在2018年校招里属于中上水平,两道编程题都不算简单,一道偏字符串处理,一道偏二叉树的遍历变形,全都是LeetCode中等偏上的难度。
第三层是AI方向匹配度,这算是商汤的特色了。试卷里出现了少量机器学习基础题,比如卷积核的作用、梯度下降的基本思想等。这类题并不深,但如果你完全没接触过AI概念,会明显感觉吃力。
1.3 为什么这套题值得反复琢磨
很多同学有个误区,觉得笔试刷题就是拼手速、拼题海。但商汤这套题给我的最大感受是:它考察的不是突击记忆能力,而是你平时写代码时有没有真正理解底层机制。比如有一道关于GCD死锁的选择题,原理不复杂,但如果只是背过“串行队列+同步任务会死锁”这个结论,而没理解队列与线程的关系,换个问法就答不对了。
这篇文章的后续部分,我会把每一类考点的具体内容、我当时的选择和思考过程、以及错题复盘全部列出来,还会补充一些我在准备这场笔试时用到的方法论。
2. iOS基础考点逐项拆解
2.1 内存管理与循环引用:最容易被绕进去的送分题
商汤笔试的iOS基础部分,内存管理是绝对重点。单选题和多选题里至少出现了3道相关内容。其中最经典的一道是这样的:
下列代码在ARC环境下,是否有循环引用?如果有,如何解决? @implementation Person @property (nonatomic, copy) NSString *name; @property (nonatomic, strong) void (^block)(void); @end self.block = ^{ NSLog(@"%@", self.name); };这道题考了两个关键点:一是block会对self做强引用,二是self又通过strong属性持有block,两者相互持有就形成了循环引用。正确答案是“有循环引用,需要用__weak修饰self后再在block内部使用”。
但这里有个容易忽略的细节:ARC环境下使用__weak修饰的变量,如果block执行前self已经被释放,weakSelf会变成nil,所以更稳妥的做法是在block内部先用__strong对weakSelf做一次强引用,再在block执行期间安全使用。我当时选择了只写__weak的答案,虽然也得分,但现在复盘觉得应该主动补充__strong才是完整方案。
另一个让我印象深刻的题目是关于copy和strong的选择。题目问NSString属性应该用哪种修饰符。很多人会凭经验回答copy,但背后的原理是:如果赋值的是一个NSMutableString,strong修饰会让属性指向同一个可变对象,后续外部修改这个可变字符串时,属性值也会跟着变,这在很多场景下是隐患。用copy则会在赋值时做一次不可变拷贝,保证属性值稳定。这种题表面考修饰符,实际考的是对对象所有权和可变性的理解。
2.2 GCD多线程与死锁陷阱
多线程考点里,最经典的一道题是这样的:
在iOS中,在主队列上执行以下代码,会发生什么? dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"hello"); });答案是死锁。原因在于主队列是串行队列,而当前代码本身就运行在主队列上,dispatch_sync会让当前线程阻塞等待新任务执行,但新任务又被排在当前任务之后,形成互相等待。
这里我想多说一句:理解GCD死锁的关键,不是背结论,而是理解“队列负责任务调度,线程负责任务执行”这个分工。串行队列一次只执行一个任务,同步提交的任务必须等待前一个任务执行完才能开始。如果你把这段代码放到全局并发队列里执行,就不会发生死锁,因为并发队列不会阻塞当前任务。
商汤笔试里还考察了关于dispatch_barrier和dispatch_group的知识点。这道题问的是:多个异步任务执行完成后统一回调,用哪个API。正确答案是dispatch_group_notify。但题目有个小陷阱:如果这些任务都是同步任务,dispatch_group_notify也会正常执行,但如果是并行队列上加同步任务,需要小心dispatch_group的enter和leave次数必须严格匹配,否则回调永远不会触发。这个细节在我后来写实际项目时确实踩过坑。
2.3 Runtime与消息转发机制
Runtime相关题目在2018年的笔试中属于拉开差距的题型。商汤考了一道消息转发的流程排序题,大致是把以下步骤按顺序排好:
resolveInstanceMethod:动态方法解析forwardingTargetForSelector:快速转发给其他对象methodSignatureForSelector:获取方法签名forwardInvocation:完整转发
正确答案是1→2→3→4。这道题看起来是送分题,但实际上很多人会把2和3的顺序记反。我当时能答对,是因为之前遇到过一个场景:某个对象收到了performSelector:调用,但当时并没有实现对应方法,我为了临时修复bug,通过forwardingTargetForSelector:把消息转发给了另一个对象。这句错误信息里显示的调用栈,帮我梳理了消息转发的完整顺序。现在看来,这种“靠实战踩坑记住的知识点”,远比背文档来得牢固。
商汤还问了一个关于isa指针的问题:Objective-C对象的isa指向什么。答案是“指向类对象(Class对象)”。这在iOS开发中非常基础,但它的引申考点是元类(MetaClass)的概念,如果你能把类对象、元类、根元类之间的关系画清楚,面试阶段会加不少分。
2.4 UIKit与界面布局
UI相关题目在笔试题中占比不小,但难度普遍不高。有一道典型的UITableView优化题,问“以下哪些方式可以提升UITableView的滚动流畅度”。选项包括:复用Cell、提前计算并缓存Cell高度、使用不透明的subview、避免在cellForRowAtIndexPath:中做大量同步操作。这四个选项在理论上都是正确答案。
但这里面有一个容易忽略的细节:prefetchDataSource在iOS 10以上能显著提升列表流畅度,但商汤这套题是2018年的,iOS 11已经发布,所以我在回答时特意提到了额外的方法,也判断出题目设的“坑”是想让大家区分静态优化与动态加载两类手段。实际项目中,我们团队做列表优化时一般分三层:第一层是缓存高度和复用Cell,第二层是图片异步加载并做尺寸裁剪,第三层才是用Instrument工具做性能分析和卡顿监控。笔试虽然只考了理论,但如果你能把这个思路讲清楚,面试官会眼前一亮。
另外一道UI题是关于Auto Layout的。题目问“在Auto Layout中,设置约束时缺少必要约束会发生什么”。答案是“布局可能不确定,出现frame为zero或约束警告”。这道题本身不难,但它引出的一个重点是:UIStackView在iOS 11以后对自动布局的简化。商汤这类AI公司做界面时经常要动态排列一些展示项,用UIStackView能大幅简化约束代码。如果备考时间充裕,我建议把UIStackView的使用也纳入复习范围。
3. 编程题与算法实战复盘
3.1 编程题一:字符串压缩算法
商汤笔试的两道编程题里,第一道是字符串压缩。题目描述大概是这样的:
实现一个字符串压缩函数,将连续出现的相同字符压缩成“字符+出现次数”的形式。 例如:"aaabcccccaa" 压缩后为 "a3b1c5a2"。 如果压缩后的字符串长度不小于原字符串,则返回原字符串。这道题的核心思路是遍历字符串,记录当前字符及连续出现次数,遇到不同字符时把上一段的统计结果追加到输出中。我写的是Swift版本,代码如下:
func compressString(_ s: String) -> String { if s.isEmpty { return s } var result = "" var prevChar = s.first! var count = 0 for ch in s { if ch == prevChar { count += 1 } else { result += "\(prevChar)\(count)" prevChar = ch count = 1 } } result += "\(prevChar)\(count)" return result.count < s.count ? result : s }这道题的难点不在遍历逻辑,而在边界条件的判断:字符串为空、循环结束后的最后一次拼接、以及压缩后长度不小于原字符串时返回原串。
我当时的做法是先把compressString写完整,再针对“空字符串”“单字符”“压缩后长度更长”这几种情况做自测。考场上编译器可能给不了太细致的提示,所以自己先在草稿纸上跑一遍用例很重要。尤其是“压缩后长度不小于原字符串”这一条,如果不加这个判断,就会把字符串压缩成更长或等长的结果,直接掉进出题人设置的陷阱。
3.2 编程题二:二叉树最近公共祖先
这道题是经典题目“二叉树中两个节点的最近公共祖先”,LeetCode原题编号236。题目描述是:
给定一棵二叉树和两个节点p、q,找到这两个节点的最近公共祖先。我用的思路是递归解法。从根节点开始,如果当前节点是p或q,则直接返回当前节点;否则分别在左子树和右子树中搜索,如果两边都找到了非空结果,说明当前节点就是最近公共祖先;如果只有一边有结果,那最近公共祖先就在那一边:
# 当时我用了Python来写这道题 class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None class Solution: def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: if not root or root == p or root == q: return root left = self.lowestCommonAncestor(root.left, p, q) right = self.lowestCommonAncestor(root.right, p, q) if left and right: return root return left if left else right这道题对iOS开发岗的考生来说,难度不算特别大,但考察了递归思想和二叉树遍历的掌握程度。我当时比较担心的是递归深度问题,如果树是一条链,递归会导致栈溢出,不过笔试的测试用例本身不会刻意构造极限情况,所以用递归是安全的。
商汤的笔试环境支持多种编程语言,我当时想的是用Python写更稳妥,因为Python处理树这种结构用指针引用比较顺手。如果你备考商汤或其他AI公司的笔试,建议至少掌握C++、Java、Python中一种,并且要熟悉链表、树、图这些基础数据结构在语言里的写法,不要只会在纸上画结构却写不出代码。
3.3 一道没做完的系统设计题
问答题是一道系统设计题目,要求设计一个iOS端的图像缓存组件。题目给了一个场景:需要从网络加载大量图片,要求支持异步加载、内存缓存、磁盘缓存,并说明缓存淘汰策略。这道题放到今天是老生常谈,但2018年让我当场写一套完整方案,还是有些手忙脚乱。
我当时回答的思路是这样的:
- 内存缓存使用NSCache,因为它会在内存紧张时自动清理对象,并且支持设置countLimit和totalCostLimit,不会像NSMutableDictionary那样有OOM风险。
- 磁盘缓存使用文件系统,以图片URL的MD5值作为文件名,控制总缓存大小和过期时间。
- 图片加载使用异步方式,然后在主线程回调更新UI。
- 缓存淘汰策略采用LRU,每次访问缓存时把对应项移到最近使用位置,超过容量时淘汰最久未使用的缓存项。
这套思路其实很接近SDWebImage的实现方式。如果让我现在重新答这道题,我会补充两个关键点:一是图片解码操作应该放到后台线程,避免在主线程解压图片导致卡顿;二是内存缓存失效时要同时考虑磁盘缓存和图片请求去重,避免同一URL在短时间内被重复请求加载。这个思路后来在我处理大列表图片性能问题时,真的直接转化成了一套成熟的优化方案。
4. AI相关题目与商汤特色考察
4.1 卷积神经网络基础题
商汤的笔试里出现了几道机器学习基础题,虽然占比不大,但如果你完全没有接触过AI知识,很容易在这些“额外题”上丢分。我记得有一道题是问“卷积层的主要作用是什么”,选项包括图像降维、特征提取、分类、归一化。正确答案是“特征提取”。
这道题对计算机视觉方向的同学来说简单得不能再简单,但对纯iOS方向的考生来说,可能有点陌生。我当时准备笔试时特意翻了一下AI基础概念,因为商汤作为AI视觉公司,就算招iOS开发工程师,也希望你对公司主营方向有基本认知。类似公司,比如旷视、依图,笔试也多有这种出题风格。
4.2 梯度下降与损失函数
还有一道选择题问“梯度下降算法中,学习率过大会导致什么问题”。正确答案是“损失值震荡甚至无法收敛”。这个考点其实很通用,任何一个做算法工程的岗位都绕不开。不过iOS岗位考到这里,更像是在筛选“有学习能力、愿意跨界”的人。
我的建议是:准备商汤这类AI公司笔试时,不用把机器学习当作大头来复习,但至少要理解卷积、池化、全连接、梯度下降、过拟合这些最基础的概念。不需要会推公式,但选择题如果遇到,要做到能选对。如果你本身对AI方向感兴趣,笔试后不妨深入看一下YOLO、ResNet这些经典模型,对后续面试和实际项目会有很大帮助。
4.3 为什么AI公司要考iOS开发者的AI知识
这个问题我后来和商汤的一名工程师聊过一次。他说这背后的逻辑其实很现实:AI公司的客户端研发工作,不只是写界面,很多时候要做模型在移动端的集成、推理性能优化、图像采集与预处理等工作。如果你不懂AI的基础概念,遇到类似问题会完全不知道从何下手。
所以这套笔试里加入AI题目,更像是一种“方向信号测试”,考察候选人对公司技术主线的敏感度。不是要求你已经会训练模型,而是希望你有基本认知、愿意学习。这一点对我后来的职业选择也有启发——准备任何公司的笔试,都要提前去了解它的业务和技术方向,这是性价比最高的备考策略。
5. 备考经验与避坑指南
5.1 笔试现场的时间分配策略
我这次笔试是一场线上笔试,总时长90分钟。我当时的策略是:先快速把选择题做完,遇到完全没把握的先标记,不恋战;然后优先做两道编程题,因为编程题分值高、区分度大;最后留15到20分钟回去处理标记过的选择题和问答题。
这个策略帮了我大忙。编程题的第一道字符串压缩,我用大概10分钟写完并自测;第二道最近公共祖先,我花的时间多一些,大约25分钟,因为递归思路虽然清晰,但边界条件还是需要多验证几组用例。如果我在选择题上纠结太久,两道编程题肯定会写不完。
给准备校招笔试的同学一个建议:笔试前先看题目分值分布,编程题往往每题15到25分,一定要留足时间。选择题哪怕全部空着只扣少数分,也比编程题没写完直接丢20分划算得多。
5.2 最容易踩的坑和连错题复盘
我这场笔试里,最近踩坑的题目主要集中在两块:一个是前面提到的GCD死锁分析,另一个是消息转发流程的排序。死锁题第一时间容易直接选择“正常执行,子线程打印”,如果对GCD的同步/异步语义不够敏感就会掉坑;消息转发排序题则容易把动态方法解析和快速转发顺序搞混。
这里列一个踩坑速查表,方便你们备考时自检:
| 考点 | 常见错误 | 正确理解 |
|---|---|---|
| block循环引用 | 认为__weak后就绝对安全 | 需要配合__strong防止弱引用nil |
| copy vs strong | 所有对象都用strong | 可变/不可变类要注意赋值时的引用行为 |
| dispatch_sync主队列死锁 | 以为只是子线程阻塞 | 当前所在队列的任务被阻塞,形成互相等待 |
| 消息转发顺序 | 快速转发和完整转发顺序颠倒 | 动态方法解析在前,快速转发其次,完整转发最后 |
| 缓存淘汰策略 | 只考虑FIFO | 实际场景中LRU更贴合图片浏览需求 |
5.3 笔试之后:如何转化为综合能力
笔试结束不代表学习结束。我考完的当天晚上,就把所有记得的题目和答案整理成笔记,给自己设置了一个复盘周期:三天内把每一道错题对应的知识点在官方文档或权威博客里找到原文,再写一遍代码或完整推导。这个习惯帮助我在后续多家公司的面试中很有底气。
我个人觉得,笔试真正的价值不是那道题的分数,而是它逼你把iOS核心知识体系重新梳理了一遍。准备商汤笔试的过程,让我系统地复习了内存管理、多线程、Runtime、网络请求和UI布局,这些内容在后续的真实项目中反复用到,属于性价比极高的投入。
如果你打算投商汤或其他AI公司的iOS岗位,我的建议是:别只刷iOS岗的题,把计算机基础(尤其是数据结构和算法)补扎实,再把机器学习最基础的概念扫一遍,就已经比大多数候选人有竞争力了。
6. 时间过去几年,再回顾这套笔试题
现在回头看商汤2018年这场笔试,很多知识点在今天仍然是iOS面试的核心。内存管理、GCD、消息转发、UITableView优化,这些技术在最新版本的iOS开发中依然是基本功。变化在于,现在SwiftUI逐渐成为主流,很多底层实现方式发生了改变,比如Swift中ARC的自动管理更完善,但循环引用的坑还是没有消失。
我对这套题最深的体会是:它不像很多公司的笔试题那样拼记忆、拼题库,而是通过题目引导你去思考“为什么”。比如为什么copy更安全,为什么主队列同步任务会死锁,为什么LRU适合图片缓存。这些“为什么”才是工程师的核心竞争力,也是考试真正想找的东西。
如果你想找当年的原题再练练手,网上可以搜到不少校招笔试复盘帖,但要把重点放在知识点本身,而不是去背题目答案。技术面试和笔试,说到底检验的是解决问题的能力,不是背书能力。提前把知识体系梳理清晰,远比刷多少道题更重要。
最后再分享一个小技巧:准备笔试的时候,可以把你不会的每一道题做成一个“问题卡片”,正面写题目,背面写考点和思路,每天抽15分钟过一遍。这个方法帮我快速建立起了完整的知识框架,亲测有效。