
商汤科技2018校招iOS开发工程师笔试第一场考点拆解与实战复盘每年八月底九月初校招笔试就开始密集轰炸了。我翻到当年参加商汤科技2018校招iOS开发工程师第一场笔试的记录感触还挺多的。商汤作为AI视觉领域的头部公司iOS岗位笔试不是单纯考“你会不会写界面”它更关注你在内存、并发、图像处理、底层原理上的基本功。这篇文章把当年的笔试内容、考点逻辑、答题思路完整还原一遍也补充了一些我这几年做iOS开发后回头看觉得更值得注意的细节。不管你是正在准备校招还是想跳槽进AI视觉类公司这份复盘都能帮你少走不少弯路。先说结论商汤这场笔试的难度属于中等偏上题型是“客观题简答题编程题”的组合题量不小时间紧张。它不考CocoaPods怎么配不考某个API叫什么考的全是“给你一个现象让你解释为什么”这类底层推理题。如果你平时只刷UI、只写业务这套卷子会很痛苦如果你把Runtime、Runloop、内存管理、多线程这些吃透了你会发现它其实有很明确的出题套路。1. 笔试整体定位与考点分布——商汤到底想考什么1.1 从招聘JD反推笔试出题思路商汤的iOS开发工程师岗位从JD上看要求很明确负责iOS客户端的架构设计、核心模块开发、性能优化并且要配合算法团队做AI能力的产品化落地。这就决定了笔试出题不会只盯着UIKit而是会往底层、性能、图像处理这些方向倾斜。我当时拿到的第一场笔试试卷整体分为三块计算机基础客观题、iOS专项简答题、两道编程题。客观题覆盖数据结构和操作系统简答题完全是iOS底层原理编程题一道算法、一道图像处理相关。这套组合释放的信号是商汤需要一个“懂底层、会算法、能落地”的iOS工程师而不是只会搭界面的熟练工。对比其他大厂的iOS笔试你会发现商汤的题更偏“原理推导”。比如它不会问你“UITableView复用机制是什么”而是会给你一个卡顿场景让你分析卡顿原因并从Runloop层面给出优化方案。这种题型对知识深度的要求是实打实的也是我建议所有准备校招的同学重点训练的方向。1.2 当年真题的题型结构与时间分配我记得笔试时长是90分钟题目数量大概在30道左右其中选择题占了大头简答题3到4道编程题2道。90分钟做30道选择题其实不轻松因为很多选择题是“以下说法正确的是”这类多选每道题都暗藏陷阱。时间分配上我踩过一个坑前20道选择题花了40分钟导致后面简答题写得很赶。这里给后来人一个建议选择题遇到没把握的先标记跳过不要恋战把时间留给简答题和编程题。简答题是按点给分你写清楚核心原理就能拿分比选择题猜一个选项划算得多。编程题两道是独立计时的第一道是标准的链表操作题第二道结合了图像像素处理要求用iOS的开发语言实现。说实话第二道题如果没做过图像处理相关的东西第一眼会有点懵但静下心看它考的仍然是基础的数据遍历和边界处理后面我会详细拆这两道题的解法。2. 选择题的隐藏考点——数据结构、操作系统与内存布局2.1 数据结构题链表的边界条件是重灾区客观题里的数据结构题链表出现的频率极高。我当时遇到的有这样几类单链表反转、判断链表是否有环、查找链表的中间节点、两个链表的交点。这些题本身不难但笔试题不会让你直接写完整代码而是给你若干种实现方案让你判断哪种的时间复杂度和空间复杂度正确。这里有个常见误区很多同学看到“反转链表”就只想迭代法看到“判断有环”就只想快慢指针。但笔试选择题经常考递归和迭代的对比、空间复杂度是O(1)还是O(n)这些细节才是真正区分度所在。比如递归反转链表实现很简洁但递归栈会带来O(n)的额外空间如果题目里明确要求空间复杂度O(1)递归方案就是错的。然后是栈和队列的应用题这类题基本就是给你一个场景让你选最合适的数据结构。比如“实现浏览器前进后退”应该用双栈“实现打印机任务排队”应该用队列“实现系统调用栈”应该用栈。这类题属于送分题但要注意题干里的隐藏条件比如是否要求O(1)时间复杂度访问最大值如果要求那就是单调栈或者辅助栈的考点了。二叉树的选择题需要掌握几种遍历方式的性质前序、中序、后序、层序。高频考点是“已知前序和中序求后序”或者“已知中序和后序还原二叉树”。这类题建议自己画图推演一遍考试的时候能省不少时间。还有平衡二叉树和二叉搜索树的判断条件以及完全二叉树的性质这些都是选择题的常客。2.2 操作系统题线程、锁与死锁的关系是必考项商汤的笔试对操作系统知识的考察集中在进程与线程、锁机制、死锁条件、内存分配这几块。iOS面试本来就会问多线程所以操作系统的线程题算是给后面的iOS专项做铺垫。我记得有道题考的是“死锁的四个必要条件”互斥、持有并等待、不可剥夺、循环等待。这道题本身不难但选项里会设置一些似是而非的干扰项比如“资源分配公平”“请求一次性完成”这些都不是必要条件。你如果只是死记硬背没有真正理解很容易选错。另一个高频考点是用户态和内核态的切换。题目会问什么操作会触发系统调用文件读写、网络请求、内存分配申请这些都会。这里有个iOS相关的延伸你在主线程里做大量的文件读写操作为什么会导致UI卡顿表面上是线程阻塞本质上就是频繁的用户态和内核态切换CPU被系统调用占据主线程的Runloop没法及时处理UI事件。锁的考题比死锁更灵活会问自旋锁和互斥锁的区别、什么场景适合用哪种锁。在iOS里对应的就是OSSpinLock、NSLock、pthread_mutex这些。尤其是自旋锁iOS 10之后因为优先级反转问题被废弃替换成了os_unfair_lock这道题商汤真的考过我当时答得一般后来研究透了才明白背后的线程优先级反转机制。2.3 内存布局题从系统到底层一条线串起来客观题里关于内存的题考的是函数调用栈、堆和栈的区别、静态变量和全局变量的存储位置。这些知识点在iOS里同样能找到对应场景Block捕获变量时栈上变量和堆上变量的处理方式完全不同这就是内存布局知识在iOS开发中的实际应用。有一道题我记得很清楚给了几段代码让判断哪些变量分配在栈上哪些分配在堆上。这道题看似简单但陷阱在于Objective-C的对象指针和对象本身的位置不同指针本身在栈上指向的对象在堆上。如果选项里把“指针在堆上”这种说法当成正确选项你就得分辨清楚。还有一道题考的是内存对齐问一个结构体在64位系统下占多少字节。这类题只要记住对齐规则就不难每个成员的偏移量必须是该成员大小的整数倍结构体的总大小必须是最大成员大小的整数倍。我当时因为记错了double的对齐规则丢了一分这种低级失误很可惜。关于虚拟内存和物理内存的关系商汤也出了一道题问的是页表和缺页中断的原理。这道题放到iOS场景里对应的就是内存映射文件mmap的机制SDWebImage的磁盘缓存、以及大文件读取时用mmap而不是read背后的原理都是同一套。如果你能答到这个层面面试官会觉得你是真的理解了而不是只会背概念。3. iOS专项简答题——底层原理才是核心分水岭3.1 Runtime与消息机制答出消息转发全流程才算过关OS专项简答题的C位必须留给Runtime和消息机制。商汤这道题问的是“向一个对象发送它没有实现的消息会发生什么完整描述消息转发流程。”这道题的完整答案分三步。第一步是消息查找objc_msgSend会先查对象的isa指针去类对象的方法缓存里找找不到就去方法列表里找再找不到就去父类链上逐级查找。第二步是动态方法解析如果一直找不到实现Runtime会调用resolveInstanceMethod让你有机会用class_addMethod动态添加实现。第三步是快速转发和完整转发resolveInstanceMethod返回NO之后系统会调forwardingTargetForSelector让你把消息转发给其他对象如果这个方法也返回nil就会走methodSignatureForSelector和forwardInvocation你可以在这里做完整的消息转发。答这道题的关键是画出完整链路而不是只答一句“会崩溃”。我当时把每个步骤的方法名、返回值含义、以及触发条件都写清楚了而且加了代码示例说明resolveInstanceMethod里怎么动态添加方法。这种详尽的回答即使文字不够精炼也会给阅卷人留下扎实的印象。另外这道题还有一个变体问法Category能否添加属性能添加实例变量吗如果能添加属性getter和setter怎么实现这道题的正确答案是Category可以添加属性声明但不会自动生成实例变量和getter/setter你需要用关联对象AssociatedObject来实现。面试官会继续追问关联对象存在哪里、什么时候销毁答案存在一个全局的AssociationsHashMap里在对象销毁时由Runtime自动清理。3.2 内存管理从ARC到weak原理一问到底商汤简答题里关于内存管理的题问的是weak修饰符的实现原理。这道题我建议准备的时候往深挖因为面试官基本都会追问下去。weak的核心原理可以拆成三块第一weak表是一个Hash表key是被指向对象的地址value是weak指针地址的数组第二当对象的引用计数降为0时dealloc会调用objc_clear_deallocating根据对象地址找到weak表中对应的数组把所有weak指针置为nil第三用__weak修饰的变量在访问时会调用objc_loadWeak和objc_storeWeak这两个函数保证读取时不会悬空。这个机制解决了什么问题最直接的就是循环引用。block捕获self、delegate属性用weak修饰、NSTimer持有target这些都是iOS开发里最经典的循环引用问题笔试和面试都爱考。我当时答weak原理的时候画了一下对象释放前后weak表的变化这个图示化的表达很加分推荐大家在笔试时也用这个思路。内存管理还有一个高频点autoreleasepool的原理。商汤这道题问的是“什么时候需要自定义autoreleasepool”。标准答案是在for循环里创建大量临时对象时、以及在使用非Cocoa环境比如写命令行工具时。更深层的机制是autoreleasepool的底层实现是一个栈结构每一个AutoreleasePoolPage存储了对象的指针page满了会新建page并双向链表连接。autoreleasepool块结束时会对page里的所有对象发release消息。如果你想把内存管理题答出彩可以再补充一个toll-free bridging相关的知识点Core Foundation对象和Objective-C对象之间的转换。ARC下使用__bridge、__bridge_retained、__bridge_transfer三者的区别以及什么时候会导致过度释放这是很多候选人答不清的细节但商汤喜欢考这种边角料。3.3 Runloop与性能优化卡顿分析题的完整解题思路Runloop的考题商汤给的是一个真实场景“主线程卡顿是什么原因从Runloop角度分析你会怎么监控和定位”Runloop的六个状态Entry、BeforeTimers、BeforeSources、AfterWaiting、BeforeWaiting、Exit都在系统源码里对应着回调。卡顿监控的思路就是监听Runloop的BeforeSources和AfterWaiting两个状态之间的耗时一旦超过阈值比如250ms就认为发生了卡顿这时去抓当前主线程的调用栈就能定位到卡顿的函数。这道题答得好的关键在于你要理解Runloop的工作机制主线程Runloop跑在kCFRunLoopDefaultMode下Sources和Timers分别在什么阶段被处理。常见的卡顿原因比如主线程做了大量同步IO、进行了复杂的CPU计算、或者发生死锁都会阻塞Runloop的消息处理导致UI无法刷新。我之前做过一个性能监控组件模仿的就是这个原理面试时把这个实践经验一讲说服力会强很多。Runloop还有一个衍生考点NSTimer在滑动时为什么不执行因为滑动时Runloop切换到了UITrackingRunLoopMode默认模式下注册的Timer不会被处理。解决方案是把Timer加入到NSRunLoopCommonModes里或者用DispatchSourceTimer。这道题商汤也问到了属于Runloop知识的实际应用必需熟练掌握。3.4 Block原理与属性修饰符这几个坑每年都有人踩Block在iOS笔试里的地位几乎和Runtime平起平坐。商汤这道题问的是“Block的截获变量机制是什么为什么__block可以修改变量”Block会截获变量的值这是它作为闭包的机制但具体到代码层面是这样的如果Block体中没有修改外部变量那么它通过值传递方式捕获变量如果Block体中有修改变量就需要用__block修饰这时编译器会把变量包装成一个结构体Block持有的是这个结构体的指针所以能实现修改外部变量的效果。还有一个常考点是Block的三种类型_NSConcreteGlobalBlock全局Block、_NSConcreteStackBlock栈Block、_NSConcreteMallocBlock堆Block。在ARC下栈Block会自动被拷贝到堆上所以你在ARC环境下写block语法几乎不会感知到栈Block的存在。但面试官会追问什么情况下Block是栈Block答案是在MRC下Block没有被copy时。这个区别当年很多人没答出来。属性修饰符的选择题也很有意思copy和strong修饰Block有什么区别正确答案是在ARC下两者等价因为ARC下编译器会自动做copy操作但在MRC下strong其实那时叫retain不会拷贝Block会导致栈上Block被提前释放。所以写Block属性用copy是更安全的习惯也符合历史规范的推荐。3.5 iOS架构设计题商汤怎么考察模块化思维商汤简答题里有一道架构题“如果你要为商汤的相册类App设计一套图片处理流程怎么做模块划分从内存、线程、缓存三个角度描述。”这道题我当时答得比较凌乱后来复盘才理清思路。内存角度图片解码后的位图数据非常大要避免一次性加载超大图用分片加载和缩略图机制必要时用downsampling降低采样率线程角度解码、滤镜、人脸检测这些耗时操作放子线程主线程只做UI刷新用串行队列保证处理顺序缓存角度用NSCache做内存缓存用磁盘缓存做持久化两级缓存共同减少重复解码。这套答案其实把iOS图像处理的一个完整链路都覆盖了如果你还能补上SDWebImage的加载逻辑、或者GPUImage的滤镜链设计会更贴合商汤的AI产品背景。我当时复盘发现这道题根本不是考你会不会写代码而是考你有没有对图像处理全链路的系统思考这也是AI公司iOS笔试和普通互联网公司的最大区别。4. 网络与系统基础知识——容易被忽略的隐性门槛4.1 HTTP与TCP笔试选择题里的常客简答题里的大题商汤笔试关于网络的选择题集中在HTTP方法、状态码、GET和POST的区别、Cookie和Session的区别这些基础题。但简答题考了一道HTTP和TCP的关系题目问“一个HTTP请求从发出到收到响应经历了哪些过程请从TCP连接建立开始描述。”标准链路是DNS解析拿到IP然后TCP三次握手建立连接发送HTTP请求报文服务器处理并返回响应客户端收到后四次挥手关闭连接。更完整的答题还需要补充如果是HTTPS在三次握手之后还有TLS握手流程HTTP/1.1默认开启Keep-Alive多个请求可以复用同一个TCP连接。这些细节展示的深度决定了这道题的档次。面试官如果对网络知识感兴趣还会深挖TCP拥塞控制和流量控制的区别。流量控制是接收方根据自身接收能力控制发送速率用的是滑动窗口机制拥塞控制是发送方根据网络状况是否有丢包调整发送速率用的是慢启动、拥塞避免、快重传、快恢复算法。答完这套网络题的分数基本就稳了。4.2 多线程与锁GCD的底层逻辑比API用法更重要多线程题在iOS笔试里的占比仅次于内存管理。商汤这道题考的是“多个线程同时读写同一个变量为什么需要加锁GCD的同步队列能替代锁吗”第一问很好答多线程同时读写共享变量会导致数据竞争结果不可预测需要加锁保证线程安全。第二问就考验理解了GCD的串行队列可以保证同一时刻只有一个任务在执行所以能保证读写任务串行但代价是性能不如精细的锁。更重要的是串行队列只能保证“任务队列的顺序执行”如果多个线程同时操作同一资源但没走同一个队列照样有数据竞争。关于死锁的题最常见的是在串行队列里调用dispatch_sync向同一个队列提交同步任务这会导致死锁。因为当前任务在等待新任务执行但新任务排在当前任务后面形成循环等待。这道题商汤笔试考过几乎也是所有iOS面试必问的题建议准备一个实际的代码示例来说明。4.3 iOS特有机制KVO与通知中心的实现原理KVO和通知中心是iOS面试的高频题商汤笔试也涉及了。KVO的题是“KVO的实现原理是什么为什么被观察对象的isa指针会改变”标准答案是当对象第一次被KVO观察时Runtime会动态创建一个子类NSKVONotifying_xxx然后把对象的isa指针指向这个子类。子类重写了被观察属性的setter方法在setter里调用willChangeValueForKey和didChangeValueForKey从而触发观察者的回调。这个动态子类机制的细节很容易被忽略但却是KVO题的核心得分点。通知中心的题是“通知中心是单例吗它的工作流程是什么”答案很简单NSNotificationCenter是单例它保存了一个注册表里面存的是通知名到观察者映射postNotification时根据名字找到所有观察者依次调用对应方法。但要注意一个坑通知的发送和接收是同线程的回到主线程更新UI需要手动切换。如果是Block方式注册通知还要注意block中引用self导致循环引用的问题。5. 编程题实战复盘——两道题写明白基本就稳了5.1 链表反转笔试高频题细节决定成败商汤2018校招iOS笔试第一场的编程题第一道就是单链表反转。题目一般是笔纸作答或在线编辑器作答要求实现函数输入单链表的头节点返回反转后的头节点。最稳妥的迭代写法是以Swift为例class ListNode { var val: Int var next: ListNode? init(_ val: Int) { self.val val self.next nil } } func reverseList(_ head: ListNode?) - ListNode? { var prev: ListNode? nil var curr head while curr ! nil { let next curr?.next curr?.next prev prev curr curr next } return prev }这段代码的核心是维护prev和next两个指针保证在修改当前节点的next之前先把下一个节点存下来否则链表就断了。这是一个经典的“三指针”操作当年笔试很多人因为忘记保存next节点写出了死循环这种低级错误很可惜。如果题目要求递归写法func reverseList(_ head: ListNode?) - ListNode? { if head nil || head?.next nil { return head } let newHead reverseList(head?.next) head?.next?.next head head?.next nil return newHead }递归的核心是“把后面的链先反转好再把当前节点接到尾部”。很多同学卡在head?.next?.next head这一步理解不了为什么是next的next指向自己。画图想明白之后这道题就通了。笔试时如果时间来得及建议把两种写法的空间复杂度差异写出来迭代O(1)空间递归O(n)空间栈深度展示你已经吃透了。5.2 图像灰度化处理商汤的特色题目考验数组操作功底第二道编程题是商汤的“特色菜”给定一张图片的像素矩阵每个像素有RGB三个通道实现灰度化处理。这道题初看是图像处理实际核心就是二维数组遍历和数学公式。灰度化最常用的方式是加权平均法公式是Gray 0.299 R 0.587 G 0.114 B。为什么不是简单平均(RGB)/3因为人眼对绿色更敏感对蓝色最不敏感所以加权系数是经过视觉研究得出的。这个细节面试官如果追问你能答上来会非常加分。给出的代码框架一般长这样Objective-C或Swiftfunc convertToGrayScale(pixels: [[(r: Int, g: Int, b: Int)]]) - [[Int]] { var result [[Int]]() for row in pixels { var grayRow [Int]() for pixel in row { let gray Int(Double(pixel.r) * 0.299 Double(pixel.g) * 0.587 Double(pixel.b) * 0.114) grayRow.append(gray) } result.append(grayRow) } return result }这道题的难点不在公式而在边界处理和数据结构的表示。你要明确传入的矩阵是否是规整的每行列数是否相同图片很大时如何分块处理避免内存爆掉以及像素值范围0到255的越界检查。这些在实际开发里都是要靠经验积累的商汤作为图像公司考这种题完全在情理之中。如果笔试要求App层面实现还要考虑图片的解码和编码问题。UIImage转CGImage再拿像素数据需要用到CGContext来绘制位图取到底层数据后处理完再生成新图片。整体流程不难但对Core Graphics的熟悉程度有要求建议准备校招的同学提前练一个“图片灰度化”的小Demo把这条链路跑通。5.3 编程题的得分技巧思路优先代码其次编程题的评分规则一般是看思路、看边界处理、看代码风格。我当年笔试有个心得先写清思路再写代码。哪怕代码有小bug只要思路对也能拿大部分分数反过来代码写得很长但没有核心思路反而容易被扣分。具体操作建议是在线编程题先写几个注释把算法步骤列出来再往注释里填代码。这样既保证自己不跑偏阅卷人也能看到完整的解题思路一举两得。另外边界情况一定要单独处理链表为空、只有一个节点、像素矩阵为空这些情况都要写清楚漏掉边界是笔试扣分最常见的理由。6. 校招笔试逆袭指南——三个月的系统备考路线6.1 备考资源与时间分配别拿两个月刷UI题如果你现在是大三或研二准备参加下一届校招我的建议是按三个月来规划。第一个月主攻计算机基础数据结构、操作系统、网络保证选择题不丢分第二个月主攻iOS底层Runtime、Runloop、内存管理、多线程、Block保证简答题能写满第三个月刷编程题LeetCode的链表、树、动态规划再加几个iOS图像处理的小项目。至于经典的iOS面试题资料比如《Objective-C高级编程》、Sunny的面试题总结、各种Runloop源码分析文章建议反复看三遍以上并自己画图复述。面试前一个月最好能做到不看资料自己把消息转发流程、weak底层实现、Runloop状态机完整讲出来讲不清楚的地方就是你薄弱的地方。6.2 避坑清单这些失误我当年都犯过第一个坑是选择题上花太多时间。笔试题量大给简答题和编程题留够时间非常重要。我的建议是选择题平均每题不超过1.5分钟不会的先标记全部做完再回头处理。第二个坑是简答题只写结论不写推导过程。阅卷人想看的是你“怎么想”的不是“记住什么”所以每一道简答题都要写出原因的展开让阅卷人看到完整的推理链条。第三个坑是编程题不处理边界。拿到题目先想好测试用例空输入、单元素、超大输入都要覆盖写代码时把guard或if判断加上。第四个坑是忽略图像处理题目。商汤的笔试题经常结合图像和AI场景建议提前做几个图像处理小工程比如滤镜、灰度化、人脸检测框既积累经验又能提高笔试时候的信心。6.3 关于这场笔试的最后一句话回头看商汤2018年这场iOS笔试出题人很聪明它没有一道题是刻意刁难人的偏题怪题但每一道题都在考察你有没有真正理解iOS运行的本质。消息转发、weak表、Runloop状态机、Block类型这些知识点你单独背都能背下来但组合在一套卷子里就会暴露你是“真懂”还是“背答案”。我当时有几道题其实答得不够透彻比如架构设计题和图像处理编程题是靠后来补课才想明白的。但正是这场笔试让我认识到iOS开发不能只停留在写界面、调API的层面底层原理的深度决定了你能走多远。这也是我想对正在准备校招的同学说的刷题很重要理解更重要把每个“为什么”想透笔试面试都会顺很多。