爱奇艺iOS校招笔试全复盘:核心考点、解题思路与备战策略
开篇:一次校招笔试,为什么值得专门复盘
先交代一下背景。2018年秋季,爱奇艺启动了当年的校招,iOS工程师岗位分为多场进行,第三场笔试是面向投递较晚、或者第一批笔试没赶上的同学。我当年完整参加了这场笔试,后来也带着这份经验带过不少学弟学妹准备校招,所以想把这套题背后的考察逻辑、每类考点的准备方式、以及我在考场上踩过的坑,完整整理出来。
先说说这篇文章适合谁看。如果你是正在准备iOS校招、或者准备跳槽大厂iOS岗位的工程师,这篇文章能帮你理清"爱奇艺这类视频业务大厂到底想考什么"。如果你只是刚学iOS不久,还没到面试阶段,也可以把这份复盘当作学习路线图——笔试中反复出现的知识点,就是你接下来半年最值得投入精力去啃的内容。
有意思的是,距离2018年已经过去好几年,但把当年的笔试题拿出来看,核心考点并没有过时。内存管理、多线程、网络请求、UI布局、基础算法,这些到今天依然是iOS面试的高频区。变化的只是技术的具体形态——比如现在面试更爱问Swift Concurrency,当年问的是GCD和OperationQueue;现在都在聊SwiftUI,当年还在死磕Auto Layout和frame布局。但底层的计算机基础和工程思维,始终没有变过。
所以这篇复盘,我尽量只讲那些"换一身外壳依然会考"的东西。
1. 整体考察思路与备考策略
1.1 爱奇艺校招笔试想筛选什么样的人
先说结论:这批题目不是单纯考"你背了多少iOS API",而是在考"你有没有软件工程师的基本素养"。整个笔试分为两个大部分——一部分是客观题和基础编程题,一部分是iOS相关的专题。客观题覆盖了数据结构、计算机网络、操作系统、Objective-C/Swift基础,专题部分涉及iOS特有机制,比如RunLoop、内存管理、多线程、界面布局、网络层封装。
这和很多公司的校招笔试结构相似,但爱奇艺的题目有个明显特点:偏向实际业务场景,而不是纯粹的理论默写。举例来说,它不会直接问你"什么是引用计数",而是给你一段有循环引用的代码,让你找出内存泄漏发生在哪一行;不会直接问"什么是死锁",而是给你三个并发任务,让你判断这个调度方式会不会卡死主线程。这种套路,明显是希望招进来的人能直接上手写业务代码,而不是还要先教一遍工程习惯。
另外很关键的一点是,爱奇艺是视频业务公司,播放页、首页信息流、弹幕、评论这些场景在题目中反复出现。如果你有视频类App的开发经验,或者至少想过"一个视频列表页应该怎么做内存管理",在审题的时候会天然有优势。
1.2 覆盖range与时间分配:两个小时怎么拿分
我记得那场笔试的总时长是120分钟,题量不大,但每道题都有一定的深度。最忌讳的做法是平均分配时间——基础选择题每道花三五分钟,结果后面的大题只剩二十分钟,写到一半就被迫提交。
我当时的时间分配策略是:选择题和填空题控制在40分钟内完成,中间的主观题(比如让你补全代码、解释输出结果)控制在30分钟,最后的综合设计题和编程题留下至少50分钟。综合题通常分值最高,而且它的评分是按思路分步给的,即使最后没跑通,只要思路清晰、代码结构完整,也能拿到60%到70%的分数。如果前面拖太久,最后的大题直接空着,那基本就没希望了。
现在回看,这个策略仍然适用于绝大多数公司的笔试,不管是iOS岗还是其他开发岗。笔试的分数密度不是均匀分布的,最后的核心大题才是拉分的关键,前面的题保证正确率即可,不需要做到完美。
1.3 别把宝押在"刷原题"上
我知道很多人在笔试前会去找"爱奇艺iOS校招原题"来背。但实际经历过就知道,除非是特别近期的题目,否则很难遇到完全一样的原题。大厂的题库通常是在一个大的题目池里随机抽取组合,你背到的可能是某道题的一个变体,选项顺序变了、数值变了、甚至考察角度直接从"让你写出代码"变成"让你指出代码哪里有问题"。
更有效的准备方式,是把知识点本身的原理吃透。比如你理解了"autoreleasepool在什么时候释放对象"的原理,无论题目以什么样的姿势去包装它——是面试题、是选择题还是代码补全——你都能识别出来。这也是我这篇复盘这么强调"为什么"的原因。
2. 核心考点拆解与解题思路
2.1 内存管理:以ARC为背景的循环引用分析
爱奇艺笔试对内存管理的考察,几乎可以确定是绕不开的。考察的角度通常不是直接问"什么是循环引用",而是给你一个对象关系图,让你判断某个对象是否被正确释放,或者给你一段闭包代码,让你指出闭包里捕获了哪些变量,会不会造成循环引用。
那时候iOS已经全面进入ARC时代,所以题目不会让你手动管理retain和release,但你需要理解ARC到底做了什么。ARC本质上是编译期插入retain/release调用的机制,它只能在编译期确定对象生命周期。遇到闭包、delegate、NSTimer这些场景,编译器无法自动推断,就可能会出现循环引用。
举一个当年考过的类似场景:一个ViewController持有UITableView,UITableView的delegate和dataSource都指向这个ViewController。与此同时,这个ViewController在viewDidLoad里创建了一个闭包,并且把它保存到自己的一个属性里,闭包内部又用到了self。问这个ViewController会不会被正确释放。
答案是:如果闭包属性是强引用(默认的strong),闭包捕获的self也是强引用,那么就会形成 self -> closure -> self 这一条循环引用链,导致dealloc永远不会被调用。解决办法是在闭包里使用[weak self]捕获列表,或者在外面用weak var weakSelf = self。
这类题看似简单,但特别容易在细节上丢分。比如很多人知道闭包要用weak,但当闭包是DispatchQueue的block时,如果没有被self持有,其实不会循环引用。笔试中需要仔细判断闭包是否被self间接持有,而不能"看到闭包就用weak"。
2.2 多线程与并发:同步/异步、队列、死锁
多线程是另一个重量级考点。2018年的时候,Swift 4已经发布了,但大部分iOS项目的核心代码还是Objective-C与Swift混编。所以笔试题通常会同时考察两个层面的内容:一是GCD、NSOperationQueue的使用,二是并发模型背后的执行顺序判断。
常见的出题方式是:给你一段代码,里面有DispatchQueue.main.async、DispatchQueue.global().async、以及一个信号量或者DispatchGroup,让你判断最后的输出顺序。这类题目表面看是考API,实际上在考察你是否真正理解"同步/异步"和"串行/并发"这两组维度的组合。
很多没准备好的同学一看到dispatch就会晕。这里我分享一个我当时总结的心法:先判断执行方法(sync还是async),再判断队列类型(串行还是并发),然后逐步推理,不要凭感觉作答。只要严格按这个顺序去分析,大部分GCD题目都是纸老虎。
还要注意另一个高频考点——死锁。经典题目是:在主队列里调用DispatchQueue.main.sync会发生什么?答案是因为主队列是串行队列,当前block正在主队列上执行,sync又向主队列提交了一个新的block,而当前block必须等sync返回,新的block又必须等当前block结束,两个任务互相等待,于是死锁。笔试里这个场景经常被包装成"用户点击按钮后执行某个操作"的形式出现,考察你是不是真的明白sync对串行队列的阻塞行为。
2.3 网络层:从HTTP到TCP的层层追问
视频类公司对网络层的考察一定会比普通App公司更深。我当时遇到的不只是"GET和POST的区别",而是从HTTP请求的完整生命周期开始问起:DNS解析过程、TCP三次握手、TLS握手、HTTP头字段的作用、HTTP/2的新特性、断点续传的实现原理。
其中有一个考察方向让我印象很深:如何设计一个支持断点续传的视频下载模块。这道题涉及到HTTP Range头字段、文件IO、内存管理、以及任务状态机的设计。即使放在今天,这也是一个很优秀的综合题,因为它同时考察了网络协议理解和客户端工程能力。
我当时的思路是这样:下载模块维护一个任务队列,每个下载任务有三个状态——等待中、下载中、已完成。下载开始时先向服务器发送HEAD请求或者带Range的GET请求,从响应头里拿到文件总大小。然后根据一定的分片大小(比如1MB)把文件切分成多个range,每个range发起独立的下载请求,写入文件时通过NSFileHandle的seekToFileOffset定位到对应的偏移量去写入。所有分片完成后验证文件完整性,再把任务状态置为已完成。
这个方案当然有很多可优化的细节,比如断点续传时如何记录已下载的分片信息、下载过程中如何应对网络切换。但在笔试中,只要你能把这个框架搭建出来,再补上几个关键边界条件的处理,就已经能拿到很好的分数了。
2.4 UI与布局:从frame到Auto Layout
UI布局在爱奇艺笔试中占的比重也不小。2018年的时候,Auto Layout已经成为主流,但笔试对这种"纯代码写布局"的题目仍然很感兴趣。常见出题方式有两种:一种给你一段手动计算frame的代码,让你指出在什么情况下会出问题;另一种给你一个目标布局的示意图,让你用Auto Layout实现出来。
第一种考察的是对不同屏幕尺寸的适配意识。手动计算frame时,如果硬编码了某个宽度和高度,在iPhone SE和iPhone Xs Max上就会出现不同的表现。正确做法是让frame基于superview的bounds去计算,或者直接用Auto Layout。笔试中很多同学会忽略一点:即使你用Auto Layout,也需要注意约束的优先级和固有内容大小,否则在内容长度变化时一样会出问题。
我还记得有一道题和UIStackView有关。2018年正好是UIStackView开始普及的阶段,爱奇艺的题目很赶时髦,让考生说出UIStackView的几种distribution的区别——fill、fillEqually、fillProportionally、equalSpacing、equalCentering。如果只是用过UIStackView但没深入理解过,很容易把fillEqually和fillProportionally搞混。简单来说,fillEqually是让每个子视图获得相同尺寸,fillProportionally是根据子视图的固有内容大小按比例分配,两者在子视图内容不一致时的表现完全不同。
2.5 算法与数据结构:视频列表场景下的实战题
算法题是筛选门槛,但爱奇艺的算法题不会很偏,主要是链表、二叉树、字符串处理、动态规划这类经典题型。和纯算法岗不同,iOS岗的算法题经常会套一层业务壳。比如"给定一个视频列表,每个视频有开始时间和结束时间,如何计算最大同时播放数",这本质上是一道扫描线算法题;再比如"实现一个LRU缓存,用于图片加载",这本来就是iOS开发中的高频场景。
我印象最深的是一道和字符串相关的题目,大概意思是:给定一个字符串,找出最长的不含重复字符的子串长度。这道题在LeetCode上属于中等难度,但如果在笔试的iOS试卷里出现,很多人会愣一下——因为刷题时一般不会把这道题和iOS关联起来。但如果仔细想,在视频搜索、弹幕关键词过滤这些场景中,字符串处理本来就很常见。算法题考的不是"你背了多少题解",而是你有没有在业务代码中锻炼过思维。
准备这类题目,我的建议是:不要把精力花在那些偏难怪的算法上,而是把经典的数据结构操作写熟。链表反转、二叉树遍历、快排、二分查找、动态规划的经典模型,这些写熟之后,无论算法题怎么变体,你至少能写出一个可运行的版本,而不会在考场上卡死。
3. 实操过程与核心环节实现
3.1 用代码演示:循环引用的检测与修复
笔试中经常要求你直接写代码来"证明"你理解了循环引用。最好的做法是写一个可以运行的小demo,同时在注释里标注出修复的关键点。我整理了一个典型的示例,你们可以当模板来用。
class VideoPlayer { var title: String var onPlayFinished: (() -> Void)? init(title: String) { self.title = title } func startPlay() { // 模拟播放结束后回调 DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { [weak self] in guard let self = self else { return } self.onPlayFinished?() } } deinit { print("\(title) deallocated") } } class PlayerManager { var currentPlayer: VideoPlayer? func setup() { let player = VideoPlayer(title: "爱奇艺独家") // 关键点:闭包内部使用 [weak self],避免 self -> player -> closure -> self 的循环引用 player.onPlayFinished = { [weak self] in self?.handlePlayFinished() } currentPlayer = player player.startPlay() } func handlePlayFinished() { print("播放完成,更新UI") } }在笔试时,我建议在关键代码行旁边用注释写出"为什么这里要weak"以及"如果不weak会发生什么循环引用链"。一方面方便阅卷人快速看到你的思路,另一方面也是提醒自己不要漏掉关键步骤。这道题除了考察weak之外,顺带考察了DispatchQueue.global().asyncAfter的用法,一箭双雕。
3.2 用代码演示:GCD 输出顺序判断的标准分析流程
有一类经典题是给一段代码问你输出顺序,我直接把一个高频示例贴在这里:
let queue = DispatchQueue(label: "com.example.serial") queue.async { print("1") queue.sync { print("2") } print("3") } print("4")很多同学第一次看到这段代码,第一反应是输出"1 2 3 4"。但真实结果是:queue是一个串行队列,外层async代码块运行在queue上,当执行到内部queue.sync时,它向同一个串行队列提交了一个新任务,而当前任务必须等新任务执行完才能继续,新任务又必须等当前任务执行完——和主队列的main.sync死锁是一样的道理,所以程序会在执行到queue.sync时直接崩溃或者卡死。
分析这类题目时,我强烈建议你们严格按照"队列类型 + 执行方法"这个顺序来推理,不要凭直觉。第一,看看这段代码是在哪个队列上执行;第二,看看调用sync/async的队列是不是同一个;第三,判断是否有等待关系。只要这三点理清了,就不会出错。
3.3 手工推演:断点续传下载模块的设计
这是当年综合题里最接近实际业务的一道,我详细说说我在笔试时的解答结构。
首先是需求分析:用户观看视频时可能中途退出,再次点击观看时希望从上次的位置继续播放,而不需要重新请求整个文件。这背后需要支持HTTP Range请求,也就是在HTTP请求头里带上Range: bytes=start-end,服务器返回206 Partial Content,并携带Content-Range响应头。
然后是客户端设计,我分了几个模块:
任务管理模块:维护一个下载任务的集合,每个任务有唯一标识(比如视频ID),任务状态包括waiting、downloading、paused、finished、failed。这个模块的核心是状态转换的合法性判断,防止从failed直接跳到finished这种非法状态。
分片下载模块:将文件按照固定大小(比如1MB)分成多个分片,每个分片发起独立的HTTP请求。每个分片下载完成后,将数据写入文件对应的偏移位置。使用NSFileHandle的seekToFileOffset来定位写入位置,避免用NSData拼接整个文件,减少内存占用。
进度记录模块:每完成一个分片,就把分片的完成信息写入本地plist或数据库。这样即使App被杀死,下次启动时也能从断点恢复。记录的信息包括:文件总大小、已完成的分片序号集合、文件在沙盒中的路径。
这个设计放到今天的面试中依然能打,因为它体现了一个iOS工程师对整个下载链路的理解。笔试中不用写出完整代码,但画出模块图、给出关键代码段、说清楚状态机设计,就已经很出彩了。
3.4 工程习惯:笔试代码里也要体现规范
很多同学觉得笔试代码只要能跑就行,其他不重要。但实际上,笔试代码的规范性会影响阅卷人对你的整体印象,尤其是在大厂校招这种简历多、需要快速筛选的场景下。我见过不少代码虽然能通过测试用例,但函数命名随意、魔法数字到处都是、没有任何注释,这类答案在分步给分时很吃亏。
我建议在笔试时养成三个习惯:第一,用有意义的命名,比如downloadVideoTaskWithID:而不是func a();第二,把关键的边界条件和设计意图写成注释,哪怕只是两三行;第三,尽量把逻辑拆成独立的小函数,保持每个函数的长度在20行以内。这三个习惯在面试手写代码环节也同样重要。
4. 常见问题与排查技巧实录
4.1 笔试环境与工具链:先解决"跑不起来"的问题
当年的笔试是在线OJ系统完成的,支持C、C++、Java、Objective-C等语言,但Swift的支持还不够完善。很多同学习惯用Xcode写代码,结果到了OJ环境,发现没有自动补全、没有语法高亮、编译器版本也和本地不一样,一下子就不适应了。
我先说一个通用的应对思路:提前去目标公司的笔试平台做模拟题。绝大多数校招笔试平台都提供往年的模拟题或者练习入口,花半小时熟悉一下代码编辑器的操作方式,能避免考场上浪费大量时间在"怎么把代码粘贴进去""System.out.println应该怎么写"这种低级问题上。
另外,建议答算法题时优先选择熟悉的语言。如果你平时iOS开发用Objective-C,但笔试OJ对Swift支持不好,可以尝试熟悉一下C++或Java的语法,因为大多数OJ对这两门语言的支持最完善。不要临时换语言硬写,很容易在语法细节上翻车。
4.2 选择题里的"陷阱":别被细节带偏
客观题是拿分的基础,但选择题的出题人经常会故意设置一些"看似正确、实则错误"的选项。比如遇到API相关的题目,两个选项只差一个关键词,比如copy和strong、atomic和nonatomic、lazy和assign,如果不熟悉的话,真的很容易选错。
我的建议是:遇到不确定的题,先用排除法排除明显错误的选项,再对比剩余选项之间的差异点。如果是在内存管理类题目中,把每个选项背后的引用计数变化画出来,比凭感觉选要可靠得多。画图虽然耗时,但在内存管理和多线程题目中,画图往往是"一步到位"的最快方式。
4.3 编译不过怎么办:第一个要查的不是语法
笔试中经常出现的情况是:时间只剩20分钟,代码写完但编译不过,提示一个看不懂的错误。这时候很多人的第一反应是盯着错误信息看半天,改来改去还是不对。我总结了一个更高效的排查顺序:
第一,检查括号是否匹配。手写代码最常见的错误就是多了一个右括号或者少了一个右括号,尤其是嵌套较多的方法调用时。第二,检查是不是少了import或头文件引用。OJ环境不会像Xcode那样自动帮你导入框架,忘记#import UIKit、import Foundation这种低级错误相当常见。第三,检查变量名是否拼写一致。手写代码时,变量名前后不一致的坑比想象中更频繁。
如果以上三项都排查完还是编译不过,直接放弃这道题,把时间留给后面的题目。笔试是限时赛,纠结在一道题上,后面的大题可能一分都拿不到。这一点我特别要提醒追求完美的同学——拿到70%的分数远比追求100%却做不完要强得多。
4.4 崩溃和超时:在线OJ的两种典型反馈
在线OJ的反馈一般有两种:运行崩溃和超时。运行崩溃通常意味着代码在某个输入下访问了非法内存,比如数组越界、空指针调用、重复释放。超时则说明算法时间复杂度过高。
针对这两种反馈,最有效的做法是在本地先用Xcode调试一遍。把OJ给出的测试用例拿过来,在Xcode中单步执行,观察哪一行出了问题。即使没有Xcode环境,也可以在代码里加一些打印语句,输出关键变量的值,定位问题区间。
遇到超时问题,优先检查是不是用了递归但没有终止条件、或者是不是有死循环。如果代码逻辑没问题,那就要考虑优化算法,比如用哈希表替代线性查找、用缓存记录中间结果。爱奇艺的视频业务中,列表页肯定会涉及大量数据的排序和过滤,这一块的算法优化能力,笔试中也会间接考察。
4.5 答题顺序与心态管理:两个小时的真实节奏
最后聊一个看起来和技术无关、但实际影响很大的话题——答题节奏。很多同学一上来就看到最后的大题很难,心里一慌,开始从难题做起,结果前面的基础题也来不及做了。
我的经验是:先通读一遍所有题目,标记出每道题的预估难度和分值,然后从自己最有把握的题开始做。这能确保你先把能拿到的分数拿到手。遇到一道卡了超过10分钟的题,先空着往下做,后面如果有时间再回来补。心态上始终提醒自己:校招笔试不是要考满分,而是要在所有人当中排到前面。稳定发挥、拿满基础分,你的排名通常就已经很可观了。
最后分享一个我自己的小习惯
现在回头看这场笔试,最让我受益的不是背下了哪道题,而是在准备过程中养成的一个习惯:每学一个iOS知识点,都会问自己“这个机制如果出成笔试题,会以什么形式考我?”。把RunLoop、ARC、GCD、Auto Layout这些知识从“会用”变成“能讲清楚”,再用笔试题来验证自己的理解程度,这套方法在后来的面试里也帮了大忙。
如果你正在准备iOS校招,不妨也试试这个思路——找一两套大厂的真题,不看答案,先自己计时做一遍,然后把做错的题对应的知识点找出来,回归源码和官方文档去弄懂原理。这个过程虽然慢,但对基础能力的提升是最扎实的。祝你们笔试顺利。
