去年年底整理网盘,翻出了当年校招季的很多资料,其中有一份正好是《欢聚时代2018校招笔试题-iOS A卷【成都场】》。说实话,看到这个文件名的一瞬间,当时考场上那种“题目量大、时间紧、有几道题一眼看不到底”的感觉又回来了。欢聚时代当时主打YY直播、游戏和语音社交,技术栈以Objective-C为主,Swift刚开始在部分团队落地,所以那份A卷考察的底子和今天iOS面试其实有不少重叠,但有些考点已经变了味道。
这两天我重新把这套卷子的考察范围过了一遍,结合当时成都场同学的回忆和我自己后来的复盘,把高频考点、答题思路和容易踩的坑整理成文。这不是押题,也不打算逐题给出“标准答案”,而是想借这份旧卷子聊清楚一件事:2018年前后的移动互联网公司在校招笔试里,到底在筛选什么样的iOS新人,以及这套筛选逻辑放到现在还有没有参考价值。
1. 欢聚时代这套卷子在考什么:能力模型的底层逻辑
很多同学拿到笔试题的第一反应是“这题我没见过”“这知识点我背过但忘了”,其实在一份设计合理的笔试卷里,每一道题背后都站着一个筛选维度。搞明白公司想通过笔试筛出什么样的人,比盲目刷题重要得多。
1.1 一套笔试题背后的筛选漏斗
2018年欢聚时代正处于直播业务高速扩张期,YY直播、游戏直播、语音房,每一个业务都对iOS客户端的稳定性、流畅度、IM消息链路有很高要求。笔试放在简历筛选之后、面试之前,它的核心目标不是“看你会多少偏门考点”,而是快速筛掉三类人:基础不扎实的、没有工程常识的、代码习惯很差的。
所以那份A卷的考察面其实很规整,典型分布大致如下:
| 考点大类 | 常见出题形式 | 考察意图 |
|---|---|---|
| OC语言特性 | 选择题、简答题 | 语言功底是否扎实 |
| 内存管理 | 代码阅读题、找错题 | 是否理解引用计数、循环引用 |
| Runtime与消息机制 | 简答、原理说明 | 对iOS核心机制的理解深度 |
| 多线程与RunLoop | 代码题、死锁分析 | 工程并发场景的处理能力 |
| 网络协议 | 简答、流程说明 | 是否真正做过网络请求 |
| UI布局与渲染 | 概念辨析、优化题 | 是否做过真实业务界面 |
| 系统设计 | 开放题 | 整体架构思维和表达能力 |
从这张表能看出来,笔试考察的不是“你背过多少API”,而是“你有没有形成一套关于iOS系统如何工作的心智模型”。我在考场上最直观的感受是:纯记忆类题目其实不多,更多题目是给你一段代码、一个场景,让你推演结果。这类题一旦理解机制,就很难丢分;不理解机制,靠猜基本没戏。
1.2 为什么同一套题要分A卷、B卷,还要分城市
标题里写的是“IOS A卷【成都场】”,说明同样的笔试在不同城市、不同场次会有不同卷子。这个操作在校招里很常见,一是防泄题,二是公司可以根据不同站点的简历情况微调题目难度。
但别被“A卷”“B卷”这个形式吓到,经历过的人都知道,同一批考点会被反复以不同形式打散重组。比如广州场可能把“Block循环引用”出成选择题,成都场的A卷就可能让你分析一段存在循环引用的代码输出结果。考点是稳定的,变的是出题角度。
这给了我们一个非常具体的备考启示:刷原题的意义不大,把每个核心考点的底层原理吃透,能以不变应万变,才是笔试准备的正路。我自己当年也犯过“背题”的错,后来发现一旦题目从“原理是什么”变成“代码会输出什么”,死记硬背立刻失效。
2. 高频考点逐个拆解:从OC语言基础到Runtime底层
既然笔试是一面“照妖镜”,那它最常照的就是iOS工程师的基本功。OC语言特性和Runtime机制是2018年校招笔试的绝对主力,而且直到今天,许多公司面试还在沿用同一套问法。
2.1 Category为什么不能添加成员变量
这是当年A卷里最典型的“送命题”,看起来不难,但能答完整的人比例并不高。
先说结论:分类(Category)在运行时可以把方法、协议合并进宿主类,但它不能直接添加成员变量。原因是成员变量会参与实例对象的内存布局,这个布局在编译期就由实例变量列表决定了,而Category是运行时才合并进去的,如果允许它改变实例大小,内存布局就彻底乱套了。
但这里有个细节,很多同学会混淆:分类里用@property声明的属性并不会自动生成成员变量和getter/setter,因为@property本质上是“声明+自动合成”的语法糖,自动合成的_ivar需要编译器在类结构里预留空间,而Category做不到这件事。
笔试题如果问到“那我想让分类有属性怎么办”,标准答案是使用关联对象(Associated Object)。用objc_setAssociatedObject和objc_getAssociatedObject可以给对象动态挂上属性值,原理是运行时维护了一个全局的AssociationMap。这个点我会在后面的经验部分重点讲,因为这里有一个很隐蔽的坑。
2.2 消息发送、动态解析与消息转发
OC的方法调用本质是消息发送,这件事基本是简答题必考。完整的调用链路是:
- 调用
objc_msgSend,沿着对象的isa指针找到类,在类的方法缓存和方法列表中查找SEL对应的IMP - 找不到时,进入动态方法解析流程:
resolveInstanceMethod:允许你动态给类添加方法实现 - 如果动态解析没有处理,走快速转发流程:
forwardingTargetForSelector:让其他对象代为处理 - 最后走完整转发:
methodSignatureForSelector:和forwardInvocation:,把消息封装成NSInvocation转发出去 - 都没有处理,最终触发
doesNotRecognizeSelector:崩溃
笔试考这个点,通常不是让你背诵链路,而是给一段方法交换代码或调用不存在方法的场景,让你分析结果。比如“怎样防止因方法不存在导致的崩溃”,答案就是在forwardInvocation里做统一兜底,把不存在的SEL记日志或者丢弃。
我还要多说一个实际答题时容易踩的坑:消息转发流程里,动态方法解析阶段(第2步)是给类方法添加实现的最佳时机,但有人会把动态解析和快速转发混为一谈。动态解析是“我自己补上实现”,快速转发是“我把消息扔给别人处理”,两者处理方式完全不同,答题时最好分开表述,把实现细节写清楚。
2.3 Block的三种形态和循环引用
Block是OC笔试里出镜率极高的点,因为它把语法特性、内存管理和工程实践串在了一起。2018年那会,关于Block最基本的一道题是说出它的三种形态:
_NSConcreteStackBlock:栈上Block,捕获自动变量,作用域结束后失效_NSConcreteGlobalBlock:全局Block,不捕获任何外部变量,存储在全局区域_NSConcreteMallocBlock:堆上Block,栈上Block被拷贝到堆上后形成
很多同学只背住了三种名称,但笔试题一定会追问“__block修饰符是干什么的”。要答全:__block修饰的变量可以被Block内修改,而且在Block从栈拷贝到堆时,__block变量本身也会被移入堆中,保证Block内外访问的是同一个变量。
循环引用的典型场景也必须会分析:self持有Block,Block内部又使用self或成员变量,两者互相持有导致dealloc不执行。标准的解决写法是:
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf doSomething]; };笔试阅卷时,阅卷人特别看重“Block内部是否先__strong再持有”这个小细节。只写__weak不加__strong虽然也能解决循环引用,但在Block执行过程中如果weakSelf提前释放,后续调用全部失效;加了__strong能保证整个Block调用周期内对象存活。这个细节在代码题里非常加分,也是过来人常说的“面试官一眼就能看出你有没有真实处理过循环引用”。
2.4 内存管理:从strong/weak到自动释放池
关于内存管理的选择题,2018年的笔试卷子特别喜欢结合ARC出题。核心考察点是:
strong/weak/assign/copy的区别weak指针为什么能自动置nil- 自动释放池的释放时机
weak自动置nil的底层机制,笔试常考:运行时维护了一张全局的弱引用表,对象被释放时,会自动遍历该对象的弱引用记录,把所有__weak指针安全地置为nil,避免悬垂指针。这个机制依赖objc_loadWeak和objc_storeWeak,里面还有加锁操作。
自动释放池这个问题,如果只答“出了作用域就释放”是不及格的。更完整的理解是:主线程的RunLoop在每个循环周期会自动创建和销毁自动释放池,autorelease对象在RunLoop休眠前或池子销毁时统一释放。这也解释了为什么大量循环内创建临时对象会导致内存峰值上涨,因为临时对象会被积压到当前池子销毁时才释放。
答题技巧方面,我建议画时间轴描述:一个RunLoop周期内,自动释放池什么时候创建、什么时候销毁、autorelease对象在哪个节点真正释放。这样比干巴巴背概念更容易拿分。
3. 多线程与RunLoop:这份卷子里最容易被扣分的地方
多线程和RunLoop是iOS笔试的“重灾区”,因为这部分内容需要真正的并发实践才能理解透彻,纯背书很难应付。成都场这份A卷里,相关题目占比不低,而且区分度很高。
3.1 GCD和NSOperationQueue的边界
一道高频简答题是“GCD和NSOperationQueue有什么区别”。很多同学能说出“GCD是纯C接口,NSOperationQueue是OC封装”,但这只是第一层。要拿高分,还需要补上:
- NSOperationQueue支持取消任务、设置任务依赖、控制最大并发数量,而GCD较难优雅地实现任务取消
- GCD可以用
dispatch_once实现单例,用dispatch_barrier_async实现读写锁,用dispatch_group实现多任务编排,NSOperationQueue原生不提供这些 - GCD是系统级线程管理的底层能力,NSOperationQueue内部也依赖于GCD的队列机制
笔试答题的层次应该是:先说二者底层关系,再说各自擅长场景,最后结合实际举一个“我这个功能为什么选GCD而不选NSOperationQueue”的例子。这种答题结构容易让阅卷人觉得你确实做过并发开发。
3.2 dispatch_sync死锁:一个必考代码题
dispatch_sync死锁几乎是每次笔试都会出现的代码题。最经典的场景是:在主线程执行
dispatch_sync(dispatch_get_main_queue(), ^{ // 任务内容 });这段代码必死。原因是主线程此刻正在执行dispatch_sync,而dispatch_sync会阻塞当前线程,等主队列里的Block任务完成。但主队列是串行队列,这个Block任务排在队列里,必须等当前主线程把dispatch_sync调用整完才能执行。当前线程在等Block,Block在等当前线程,形成互相等待。
另一个容易忽略的死锁场景是:在一个串行队列里调用dispatch_sync到同一个队列,同样死锁。原因是串行队列同一时刻只允许一个任务执行,当前任务还没结束,队列不可能取出下一个任务。
笔试答案只写“会死锁”是不够的,最好把“为什么死锁”“哪个线程等谁”写清楚。阅卷人看的是你有没有真正理解串行队列和阻塞的含义,而不是背诵结论。
3.3 RunLoop的Mode切换和定时器
RunLoop相关题目在2018年校招里也经常出现。最经典的例子是:在UIScrollView滑动时,NSTimer失效。原因就是RunLoop切换到了UITrackingRunLoopMode,默认模式下注册的Timer没有加入commonModes,所以滑动时不被唤醒。
标准解法是把Timer加入NSRunLoopCommonModes,或者改用CADisplayLink/DispatchSourceTimer。笔试如果考到这里,答题时最好补充一句:commonModes并不是一个真实存在的Mode,而是一个包含多种Mode的集合,它会同步到所有声明了它的Mode。
另外RunLoop有多个观察点,kCFRunLoopBeforeWaiting和kCFRunLoopAfterWaiting这两个状态和自动释放池的释放时机直接相关。如果能把RunLoop状态机、自动释放池、界面滑动卡顿三者串起来讲,这道题基本就是满分水平。
3.4 容易忽略的“并发队列与线程爆炸”
现在回头复盘,当时的卷子里有一类题目看起来简单但特别能拉开差距,它不给代码,而是给描述性场景:“一个直播App的消息服务,同时有多条并发任务在拉取数据,你如何控制并发?”这个场景看似考察网络请求,实际是考察并发控制。
2018年前后,很多App遇到线程爆炸,原因就是大量任务被提交到全局并发队列,而全局并发队列会根据系统负载动态创建线程,任务多了线程数就失控。合理的方案是:
- 用
NSOperationQueue设置maxConcurrentOperationCount - 或用
dispatch_semaphore限制同时执行的任务数 - 或者通过自定义串行队列做任务拆分
答题方向如果只写“用并发队列”就是踩了坑,因为面试官想听的恰恰是对“过度并发”的警惕。结合直播场景来说,消息服务应该用串行队列保证消息顺序,同时用并发队列处理图片下载等无顺序要求的任务。这种“场景化回答”非常能体现工程素养。
4. 网络、存储与UI布局:工程化能力的考察重点
除了语言底层和并发机制,笔试还有一个板块专门考察工程化基础:网络协议、数据持久化、UI布局和渲染优化。这些内容看起来零散,但其实每一项都和“能不能独立负责一个真实业务需求”挂钩。
4.1 从三次握手问到HTTPS:网络题怎么答才算完整
网络题在校招笔试里几乎是必出的,最常见的问法是“简述TCP三次握手过程和四次挥手过程”。这道题听着简单,但很多人写不清为什么需要三次而不是两次。
三次握手的核心是:双方都需要确认自己和对方的收发能力正常。第一次握手,客户端告诉服务器“我能发”;第二次握手,服务器告诉客户端“我能收也能发”;第三次握手,客户端告诉服务器“我也能收”。只有三次握手才能让双方都确认收发通道正常。如果只有两次,服务器无法确认客户端的接收能力。
往前一步,问到HTTPS握手时,要能说清楚TLS的流程:客户端发起请求生成随机数、服务器返回证书和随机数、客户端验证证书并生成预主密钥、双方基于预主密钥计算出会话密钥、之后用对称加密通信。证书验证的核心是数字签名和CA信任链。
移动端网络题的加分项是HTTPDNS和连接复用。可以提“网络请求默认使用HttpURLConnection/NSURLSession的keep-alive机制,长连接场景下用HTTP/2多路复用”,这会让阅卷人觉得你做过App网络层的真实调优。
4.2 SQLite与FMDB:为什么要手写增删改查
2018年笔试对数据持久化的考察频率很高,因为聊天、直播这类场景对本地缓存和消息记录要求很高。考试通常不是让写完整项目,而是让你建一张表、写几条SQL。
比如出一道“设计消息表结构”,基本答复是:
CREATE TABLE IF NOT EXISTS message ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL, content TEXT, timestamp REAL, is_read INTEGER DEFAULT 0 );更高级的回答会主动加上索引:
CREATE INDEX IF NOT EXISTS idx_timestamp ON message(timestamp);为什么加索引?因为IM消息列表最核心的查询是按照时间倒序拉取,没有索引的话全表扫描会随着消息量增长迅速变慢。这个“主动优化”的细节,在笔试中很容易被忽略,但在真实业务中极其重要。
另外FMDB的线程安全性也是常考内容:FMDatabase不是线程安全的,不能在多个线程同时使用同一个实例;正确做法是使用FMDatabaseQueue,它在内部通过串行队列保证所有数据库操作顺序执行。
这里的实操经验是:SQLite题千万别只答增删改查,能主动写出事务操作或索引,你的答案层次会高出一截。因为事务能保证多条消息批量写入时的原子性,索引则说明你考虑过数据量增长后的查询性能。
4.3 frame和bounds的区别
一道看似简单却筛掉不少人的概念题是“frame和bounds有什么区别”。这道题考察的是对视图坐标体系的理解。
简单说:frame是相对于父视图坐标系的位置和大小;bounds是相对于自身坐标系的位置和大小,并且修改bounds的origin会影响子视图的布局。
具体例子:一个View的frame是(100, 50, 200, 100),它的bounds默认是(0, 0, 200, 100)。如果把这个View的bounds改成(0, 0, 100, 50),虽然它的frame不变,但子视图会以这个新的原点来布局,结果就是所有子视图整体向左上角移动。
笔试考这道题,很多同学能答出定义,但答不出“修改bounds的origin后子视图会移动”这个应用场景。如果能把UIScrollView的滚动原理(通过修改bounds实现内容偏移)挂上,这道题就有了工程意义,阅卷人也能看出你是“用过”而不是“背过”。
4.4 离屏渲染与卡顿优化
性能优化题在2018年的笔试里不算最多,但一旦出现,很可能就是决定是否进入面试的区分题。最常考的是离屏渲染:为什么给UIView设置圆角加masksToBounds会导致离屏渲染?
原因在于:圆角加裁剪意味着图层需要先被渲染到一个离屏的缓冲区,再进行圆角裁剪和合成。离屏渲染本身不是洪水猛兽,但如果Cell里大量视图同时开启圆角+阴影,GPU的离屏缓冲区会被耗尽,触发性能瓶颈。
优化方案通常写三个方向:
- 使用
UIBezierPath画出圆角路径并设置layer.cornerRadius配合shouldRasterize,但要小心shouldRasterize的滚动卡顿 - 直接让美术切图,用带圆角的图片替代动态圆角裁剪
- 或者使用
CAShapeLayer做遮罩,避免触发整棵视图树的离屏渲染
为什么笔试爱考这个?因为公司希望招到的人上线后不会因为小小的圆角写法就让列表掉帧,尤其是欢聚时代这种以音视频和聊天室为核心业务的App,消息列表和直播间的流畅度就是生命线。这个考点背后是公司对“线上质量”的焦虑。
5. 如果卷子里有一道“设计题”,你应该给出怎样的答卷
除了客观题和代码题,校招笔试的压轴位置通常会有开放设计题。很多人一看到开放题就懵,觉得没有标准答案,无从下手。实际上开放题最有套路,也最能拉开差距。
5.1 设计一个图片加载库
图片加载库是iOS笔试开放题里的经典题目,因为它把异步、缓存、并发、内存管理、UI更新全部串在一起,特别适合考察综合能力。答题时不要上来写代码,先铺框架。
第一步,需求分析:图片加载要快,不能阻塞主线程;要支持内存和磁盘缓存;要处理重复请求;要防止图片错乱。
第二步,接口定义:核心是loadImageWithURL:completion:,支持占位图、进度回调、取消操作。这一步要体现面向协议的设计思想,而不是写死一个类。
第三步,时序设计:请求图片时先从内存缓存查,没有再查磁盘缓存,都没有才发起网络请求。网络返回后先解码、压缩到适当尺寸,再写入缓存,最后回到主线程更新UI。
第四步,边界处理:内存警告时清空内存缓存;同一个URL的并发请求要合并,避免发N次请求;TableView滑动时,Cell复用后上一次的图片请求要支持取消,防止图片错乱。
开放题答题的隐藏加分项,是主动提到“避免在主线程解大图”和“在子线程进行图片解码”。很多校招新人不知道UIImage创建时不会真正解码,直到绘制时才解码,而解码默认发生在主线程,会造成掉帧。能写出这一点,说明有真实调优经验。
5.2 设计一个IM消息列表
欢聚时代是做直播和语音社交的,IM场景贯穿整个产品。笔试里的设计题如果问“设计一个聊天室消息列表”,不要只把它当普通列表做,要抓住IM系统的核心问题:消息有序、消息去重、分页加载、增量同步、本地缓存。
比较完整的回答是:数据模型包含消息ID、发送者、内容、时间戳、消息状态(发送中、失败、已读);本地存储使用SQLite,以消息ID建表并加时间索引;列表数据源保持一个有序数组;收到新消息时先根据消息ID去重,再插入到正确位置;下拉加载历史消息时从数据库分页读取,并和当前数组做合并去重。
增量同步部分最好提一下:进入聊天室后先拉最近50条,建立本地分页游标,后续通过长连接接收增量消息。如果网络断开,重连后需要做消息补偿拉取,拉取范围是本地最大消息ID之后的所有消息。
答题时如果能画出“数据在内存、数据库、网络三端如何流动”的示意,这件事会非常加分。面试官真正想看到的不是你会写UITableView,而是你能不能设计出一个经得住消息量大、弱网环境考验的方案。
5.3 设计题通用的作答框架
开放题其实是有答题套路的,我在复盘了多份笔试评语之后总结了五步:
- 需求分析:列出该功能的用户场景和非目标,明确边界
- 接口设计:定义外部调用方式,尽量面向协议
- 模块拆分:把系统拆成网络层、存储层、业务层、UI层
- 时序流程:用文字描述一次完整操作从开始到结束的流程
- 容错与优化:补齐异常处理、性能优化、埋点监控方案
这个框架的好处是,哪怕你对某个技术细节不够熟悉,也能从总体结构上让阅卷人看出“这个候选人设计过系统”,而不是“只知道堆API”。笔试时间紧的时候,框架能帮你在有限时间里输出一份逻辑完整的答案,避免想到哪写到哪。
6. 现在回头看:这套卷子给我的三个实战启示
把2018年成都场的卷子完整复盘完,我最深的感受是:技术会过时,但考察的能力模型不会过时。那套题里涉及的消息转发、Block循环引用、RunLoop、SQLite索引、离屏渲染,放在今天的面试里依然是高频问题,只是换了一种问法,套了一层新壳。
第一个启示是,知识点不能孤立记忆,要串成系统。比如Block循环引用不是背一句“用__weak解决”就行,它连着内存管理、dealloc时序、局部变量作用域,甚至RunLoop释放时机。我在后面带新人时,会让他们把每一个“考点”做成一张关联图,由一个点往外扩散,直到能讲清楚它和自己做过的哪个线上Bug相关。这种复习方式比刷一百道题有用。
第二个启示是,手写代码题存在“隐形成分”。同一道代码题,A同学只写答案,B同学写答案的同时带注释、处理边界、注意变量命名,阅卷观感完全不同。我在模拟面试中反复提醒:笔试时先写思路,再写实现;对可能出现nil的地方做防护;命名使用系统认可的规范;写完回头复查是否有循环引用。这些“隐形成分”不影响代码本身对不对,但会影响阅卷人判断你是不是一个靠谱的工程师。
第三个启示是,这套题里我当年最头疼的设计题,反而是现在工作里最常用的能力。设计图片加载库、设计IM消息列表,这些场景几乎每天都会变个样子出现在需求里。能答好开放题的人,往往不是懂得最多的,而是思路最清晰的。思路清晰来自真实项目的反复捶打,没有捷径。
如果你正在准备下一场iOS笔试,我的建议很直接:翻出你手上最近的笔试回忆题,别看考点清单,先试着从“这个知识点扣在哪个环节,它到底解决什么问题”的角度重新梳理一遍。把基础考点吃透,比囤一堆“面经”管用得多。欢聚时代这份旧卷子本身已经成了历史,但它背后那套“考察基础、考察工程观、考察设计思维”的筛选逻辑,到今天仍然值得每个iOS开发者认真对待。