iOS校招面试复盘:从内存管理到Runloop与算法手写 1. 写在前面为什么我还在翻2018年的iOS校招题先说个背景我自己是2019届毕业2018年秋季参加了字节跳动的校园招聘投的正是iOS方向而且恰好被分在了第三批面试。这个时间点很有意思因为2018年是iOS开发者心态发生微妙变化的一年Swift已经发布四年但ObjC依然是国内大厂业务线的主力App Store审核政策开始趋严iPhone X带火的刘海屏适配问题让每个客户端工程师都得重新审视页面布局逻辑。现在回过头来看这批面试题我依然觉得它们很能代表当时头条系客户端团队的考察风格——不跟你玩偏题怪题所有问题都贴着日常开发场景但深度可以一直往下探。我一直建议准备面试的学弟学妹多看看这类真题复盘不是因为题目会原封不动再考一遍而是通过题目能反推出面试官脑子里那棵“知识树”是怎么长的。这篇文章我就以自己的亲历记忆为主线把2018校招iOS方向第三批涉及的考察点重新梳理一遍包括当时我在现场是怎么答的、哪些地方卡壳了、后来复盘时找到的正确思路以及如果放到现在再看这些考点又有了哪些新的变化。考虑到正文和关键词给的信息比较零散我主要围绕“iOS校招面试考察逻辑”展开尽量还原一个真实完整的面试场景。需要说明的是距离2018年已经过去几年我无法做到逐字逐句还原当时的每一道题目下面整理的内容是我结合记忆和同批候选人交流后的“复原版”价值在于考察点的梳理而不是照搬原题。2. 第一批、第二批、第三批批次背后的招聘节奏与考察差异很多同学可能不太理解为什么校招要分“第三批”这种说法它到底意味着什么。实际上字节这种大厂在秋招季往往采用“滚动笔试滚动面试”的形式一批就是一轮笔试时间窗口通过后才进入面试流程。2.1 第三批在时间节点上的特殊性2018年头条的校招节奏大致是第一批在8月初开放、8月中旬笔试第二批在8月底到9月初第三批则落在9月中下旬。第三批有个典型特征——候选人数量最多因为很多学校的学生在这个时间点才结束暑期实习、正式开始找工作同时一些在前两批面试中挂掉的人已经进入人才库不再有资格重复投递相当于竞争池被清洗过一轮。从面试官角度来说第三批的面评标准并不会比前两批更宽松相反因为HCheadcount可能已经消耗了不少后面的名额反而更精贵。所以你如果被分到第三批不需要焦虑“是不是因为简历不够好”分到哪一批更多是简历投递时间和笔试预约选择的自然结果。2.2 批次靠后如何调整准备策略我的真实体感是前两批面试题往往更偏“常规考点”越到后面的批次越可能出现一些结合业务场景的追问。这不是说第三批刻意增加难度而是面到后面面试官已经形成了一套“由浅入深”的提问节奏前面的题目答得快、答得准才有机会进入更深层的追问。所以如果你的面试批次靠后准备策略上要注意两点。第一基础题不能有侥幸心理只要是高频考点都要能像背乘法口诀一样脱口而出像Runloop的运行模式、KVC的赋值查找顺序这类题在第三批出现的概率非常高。第二要准备“第二层”和“第三层”追问比如面试官不仅会问“ARC下什么时候需要用weak”还会接着问“weak变量在运行时是怎么被置为nil的SideTable和weak_entry_t的存储结构是什么”再往下还会问“如果对象没有注册到SideTable里weak操作会发生什么”。每一层都要有清晰的答案。3. iOS知识体系考察从内存管理到UI布局的完整链路iOS方向的校招面试核心永远是围绕着Objective-C的运行时机制、内存管理、多线程、UI事件传递这几大块来转。2018年这批也不例外。3.1 内存管理ARC、MRC与循环引用的边边角角先说一下内存管理的考察方式。面试官基本不会直接扔一句“讲讲ARC是什么”而是会从一个很实际的场景切入“在一个控制器里用block做回调为什么需要weakSelf”我在第三批面试中遇到的情况是面试官让我写一段代码创建一个持有block属性的对象然后在block内部访问self的属性问我这会不会造成循环引用以及如何验证。这背后涉及的知识点包括ARC下的引用计数管理strong/weak/copy/assign修饰符的本质区别Block对捕获变量的处理方式为什么局部变量默认是值拷贝__block和__weak各自的作用循环引用的形成条件对象持有blockblock又持有对象验证方式用Instruments的Leaks工具或者重写dealloc观察是否被释放实际写代码时我自己当时犯了一个小错误给block属性用了assign而不是copy。在MRC时代block属于栈上分配需要copy到堆上才能安全保存ARC下编译器会自动处理多数情况但显式写copy依然是规范做法。面试官借这个点追问了一句“为什么很多公司代码规范要求block属性必须写copy”我给出了“兼容旧编译器、保证行为语义明确、避免栈block在函数返回后失效”三层解释才算完整。关于循环引用的检测还有一个容易被忽略的点在block内部只要直接使用了self哪怕只是访问self.view也会强引用self。所以不是每个block都要weakSelf要根据block是否被self持有来判断。我当时答到这里面试官点了点头然后追问了一个更细的问题“如果block被一个单例对象持有内部拿到的weakSelf还需要转strongSelf吗”这个问题其实考的是“weakSelf在block执行期间可能变nil”的竞态场景正确做法是__weak __typeof(self) weakSelf self; [someSingleton doSomethingWithBlock:^{ __strong __typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } }];如果不转strongSelf在block执行过程中self已经被释放weakSelf会变成nil后续代码就失去了执行上下文。转成strongSelf之后block执行期间self至少不会被释放逻辑上是安全的。3.2 Runloop、KVO与线程高频追问的“三层深入”打法Runloop几乎是所有iOS面试绕不开的题。2018年考察的典型问法是“Runloop和线程是什么关系它有哪些运行模式main runloop在什么时候会被唤醒。”我当时从线程生命周期切入说明Runloop是线程内部的一个事件循环机制每个线程都有且只有一个Runloop主线程的Runloop默认运行子线程的Runloop需要手动获取并run。然后列出了NSDefaultRunLoopMode、UITrackingRunLoopMode、NSRunLoopCommonModes几个模式解释了滑动时为什么定时器会被暂停、如何用CommonModes解决。面试官紧接着问了KVO的实现原理。这个问题我很熟所以答得比较顺畅利用Runtime动态生成一个子类NSKVONotifying_XXX重写被观察属性的setter方法在setter内部调用willChangeValueForKey:和didChangeValueForKey:从而触发监听回调。如果没有使用KVO对象实际是原来的类一旦注册了KVO监听对象的isa指针会指向生成的新子类。但面试官没有停在这里他接着问“KVO的自动触发和手动触发有什么区别什么时候需要手动触发”这个问题的背景是如果我们在setter里没有走KVO的promise机制而是直接在内部修改了成员变量那么即使属性值变了监听方也收不到通知。手动触发的方式是调用willChangeValueForKey:和didChangeValueForKey:这在做原子属性更新、批量修改属性不需要频繁通知时非常有用。我当时因为没实际用过手动触发这个点答得比较勉强现在回想起来如果当时能主动举个例子——比如在一个对象里同时更新多个属性希望只触发一次KVO通知那么可以在willChangeValueForKey和didChangeValueForKey外层做一次包夹——回答会更有说服力。3.3 多线程方案辨析从GCD到NSOperation的使用边界多线程的考察重点永远是那几个GCD的队列类型、同步/异步组合、死锁问题。2018年这批面试里面试官让我解释一下自定义串行队列和主队列的区别随手写了一段代码dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(hello); });然后问我这段代码会怎样。我在白板上分析dispatch_sync是同步等待如果当前执行这个调用的是主线程那么主线程等待主队列上的block执行但block被放到主队列后必须等主线程空闲才能执行而当前主线程正卡在等待中于是形成了互相等待——死锁。如果换个场景在子线程里执行这段代码就不会死锁因为主线程可以继续处理主队列的任务。这个问题算是GCD的经典送分题但很多候选人会在“主队列同步任务”的死锁条件上记混。我当时给了一个方便的记法死锁的本质是“当前线程正在等待一个需要当前线程空闲才能执行的任务”不用刻意背场景分析清楚等待关系即可。面试官顺着多线程继续问NSOperation和GCD的取舍。我的回答是NSOperation是对任务的更高层抽象支持依赖关系、取消操作、设置最大并发数而GCD更轻量、更底层。在业务开发中网络请求的并发编排建议用NSOperationQueue管理而针对延迟、异步回主线程这类轻量操作直接用GCD即可。后来做实际项目我发现NSOperation的completionBlock在并发队列里的执行线程是不确定的所以如果需要更新UI内部仍然要自己切回主线程这个细节在面试时没有聊到——如果当时能说出这个坑应该会加分。3.4 UI布局、事件传递与iOS 11适配2018年前后iOS 11刚刚发布面试官对适配问题的关注度是很高的。关于UI这一块我记得被问到的问题有Auto Layout和Frame布局的使用场景如何理解UIView和CALayer的关系hitTest:withEvent:的调用流程在iOS 11之后navigationBar的背景设置方法有哪些变化前三个问题相对基础我很快就答完了。到第四个问题我如实说“navigationBar的布局从UINavigationBar的frame延伸到安全区iOS 11新增了safeAreaInsets和UILayoutGuide所以设置背景色时需要考虑滚动内容是否会被导航栏遮挡。”这个回答不算深入但至少说明我对版本变化有感知。如果时间回到现在适配主流机型时大家已经普遍使用safeAreaLayoutGuide但2018年很多人还停留在self.automaticallyAdjustsScrollViewInsets YES的旧时代。面试官这个问题的本质是判断候选人是否养成“随版本更新修正知识体系”的习惯。事件传递的追问比较有意思。面试官问“如果两个兄弟view重叠点击重叠区域哪个view会收到事件”我回答的是hitTest会从父视图的subviews里倒序遍历最后添加的subview排在数组最后所以会先被检测如果它的hitTest返回nil再往前找。这道题我后来发现很多候选人答错主要是把“后添加的上层优先”理解成了“先添加的优先”。4. 计算机基础iOS工程师躲不开的算法与数据结构别以为客户端开发就不用考算法2018年字节跳动所有技术岗的面试都有手写代码环节。就iOS方向第三批的体验来看算法难度属于“LeetCode中等偏下”但考察方式很有讲究。4.1 白板手写链表反转的多种写法我遇到的第一道手写题是“反转单链表”。这道题本身不难但面试官要求我用递归和迭代两种方式分别写一遍并讲清楚各自的复杂度。迭代写法比较直观typedef struct Node { int value; struct Node *next; } Node; Node* reverseList(Node* head) { Node* prev NULL; Node* cur head; while (cur) { Node* next cur-next; cur-next prev; prev cur; cur next; } return prev; }递归写法需要想清楚“反转之后头结点是谁”Node* reverseListRecursive(Node* head) { if (head NULL || head-next NULL) { return head; } Node* newHead reverseListRecursive(head-next); head-next-next head; head-next NULL; return newHead; }面试官追问的是“递推关系里为什么最后一个元素会成为新的头”我画了一个三节点的链表图从最深层递归往上回溯说明每一层都把当前节点的下一个节点的next指回当前节点同时把当前节点的next断开。这样讲完之后面试官没有再深挖但我意识到他考察的关键不是在背解法而是能不能在白板上讲清楚递归的栈帧回溯过程——这一点其实是很多“会写但说不清”的同学的弱点。4.2 二叉树层序遍历与锯齿形遍历第二道算法题是二叉树的层序遍历并要求按“之字形”锯齿形输出。层序遍历用队列实现核心是记录每层的节点数量from collections import deque def zigzag_level_order(root): if not root: return [] result [] queue deque([root]) left_to_right True while queue: level_size len(queue) level [] for _ in range(level_size): node queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) if not left_to_right: level.reverse() result.append(level) left_to_right not left_to_right return result这道题我写得比较顺因为LeetCode的103题就是这个原题。但面试官后面加了一个约束“如果要求原地反转不调用数组的reverse方法你怎么做”我给出的解决方案是依然正常按层收集到临时数组中但输出时奇数层从数组尾部遍历偶数层从头部遍历本质上就是省掉一次reverse的O(n)操作。这种优化在实际业务中意义不大但面试官想考察的是候选人是否理解reverse本身的时间开销。4.3 计算机网络与操作系统基础不牢后面没法聊除了算法计算机基础也是躲不开的。2018年iOS方向面试里我印象最深的是HTTP和TCP相关的问题。面试官问“HTTPS的握手中客户端是如何验证服务器证书的”这个问题要答得完整大致分四步客户端收到服务器的证书链先检查证书是否由受信任的CA签发即验证证书链验证证书域名是否与当前访问的域名一致验证证书是否在有效期内是否被撤销如果证书验证通过客户端生成一个随机数作为对称加密密钥用服务器的公钥加密后发给服务器我当时只答到了前两步面试官提醒之后才补全了后两步。这个知识点在iOS开发里的实际落点就是ATSApp Transport Security——2017年苹果将ATS的强制要求延期到2018年所以2018年面试问HTTPS恰逢其时。如果当时能再结合ATS的配置来说比如为什么要在Info.plist里设置NSAppTransportSecurity效果会好很多。操作系统层面问到的是“进程和线程的区别”以及“死锁产生的四个条件”。这部分没有太多iOS特性就是标准答案互斥、请求与保持、不可剥夺、循环等待。面试官补充了一个“如何避免死锁”的问题我从“破坏循环等待条件”的角度答了“资源有序分配法”并引用了银行家算法的思路。后来我在实际开发中处理多个线程同时访问多个资源时都会下意识地检查是否存在“A持有资源1等待资源2、B持有资源2等待资源1”的路径这个习惯就是从这道题来的。5. 实战深挖从招聘JD反推大厂iOS工程师的素质模型面试不仅仅是做题它还会通过项目经历和开放式问题来考察工程素养。2018年第三批面试的最后一个环节面试官问了我一个很开放的问题“如果让你设计一个IM类App的消息列表页面你会怎么处理性能问题”5.1 消息列表性能优化一张白纸上的架构推演这个问题没有标准答案考察的是从数据到UI的全链路思考能力。我当时的回答分三层数据层消息模型实时变化需要做增量更新而不是全量刷新。可以用Diff算法计算变化集合避免频繁reloadData。UI层对于高帧率滚动场景cell尽量用纯Auto Layout会导致频繁计算可以考虑用文本预排版比如把NSAttributedString提前计算好boundingRect和异步绘制。线程层消息接收和UI更新不能互相阻塞数据解析放在子线程主线程只做UI更新。面试官听完后追问了一个很细的点“异步绘制的时候为什么主线程不会卡顿”我说因为文本内容在子线程已经渲染成位图主线程只需要把已经生成的位图提交给Core Animation去合成所以耗时的文本布局、表情混排解析都避开了主线程的UI更新逻辑。面试官对这个回答比较满意让我现场画一下异步绘制的流程。当时我画了一个简陋的流程图子线程创建上下文把文本用CoreText绘制到CGBitmapContext中生成CGImage主线程拿到CGImage后把它赋值给layer的contents。这个流程现在看依然是异步绘制的主流方案之一只不过后来多了更多细节比如要处理粗体、链接点击区域、表情图片的占位等。5.2 项目经历的追问逻辑做没做过不重要讲没讲清楚才重要2018年校招时大多数候选人都没有多少大型项目经验所以面试官通常不会期待你有宏伟的作品而是更关注“你在自己做过的东西里承担了什么角色、遇到了什么困难、怎么解决的”。我当时讲的是一个课程表类App的课程提醒功能用到了本地通知和CoreData。面试官追问“如果用户修改了系统时间你的提醒逻辑会不会出错”这个问题我当场没完全答上来因为当时我的项目里没有做系统时间变化的监听。面试官没有批评而是提示我可以用UIApplicationSignificantTimeChangeNotification来监听系统时间变化并在收到通知后重新计算所有提醒。这个小追问让我印象很深。它告诉我大厂面试官看重的不是项目的复杂度而是候选人有没有“边界条件”意识——也就是做需求时会不会主动考虑异常情况。后来我自己带实习生时也经常用类似的方式提问“如果用户手机没网怎么办如果用户改了时区怎么办”这类问题比“用了什么框架”更能反映一个人的工程成熟度。5.3 开放式问答为什么选择iOS怎么看跨平台2018年还有一个绕不开的话题——跨平台方案。当时React Native已经火了一段时间Flutter还在预览阶段字节内部自研的跨端方案还没有完全公开。面试官问我“既然跨平台这么热为什么还要投iOS原生方向”这道题没有标准答案但我记得自己的核心观点是跨平台方案解决的是“多端复用”的效率问题但在需要深度定制系统能力、极致性能的场景下原生依然有不可替代的价值。同时作为一个iOS开发者理解底层运行机制、内存模型、UIKit的渲染链路这些能力在跨端混合开发中同样适用——无论是RN还是Flutter最终都要通过原生桥接或渲染引擎来调用系统API。从现在的视角回头看这个回答依然站得住脚。iOS原生技术栈虽然在应用层被跨端方案蚕食了一部分但底层系统能力的理解依然是客户端工程师的核心竞争力。而且2018年面试我们这批iOS候选人的工程师后来有不少人转去做跨端底层了但他们的iOS底子反而变成了优势。6. 三轮面试流程模拟现场时间分配与心态控制校招面试通常是三轮技术面加一轮HR面2018年第三批的流程也基本如此。每一轮侧重点不同做好时间分配和心态控制很关键。6.1 一面基础题为主节奏最快一面大约45分钟前20分钟是项目介绍加基础知识问答后25分钟是算法题。这一面主要筛掉基础不扎实的候选人所以问题不会太偏但回答要快、要准。自我介绍控制在2分钟以内不要复述简历基础知识题遇到不会的先承认“这个点我了解不深”再尝试给出部分理解不要瞎编算法题如果思路卡壳先讲暴力解法再逐步优化我当时一面结束时面试官让我反问一个问题我问的是“字节iOS团队目前主要用Swift还是ObjC”。这是一个安全且有信息量的问题面试官也乐意回答。反问环节是展示你思考深度的机会最好不要问“加不加班”这种对技术面试没有增益的问题等到HR面再谈。6.2 二面深度为主会追问到“答不出来”为止二面一般是team leader或资深工程师问题会更开放、更有压迫感。比如一面问“ARC下什么时候用weak”二面可能就会问“weak实现里SideTable的底层结构”。这种追问本质上是在探测你的知识边界所以就算被问住也不用慌冷静地说“这个底层细节我没有深究过我目前的理解是……”然后把自己能讲的部分讲清楚就行。还有一个常见的二面开场“说说你遇到过最复杂的bug。”这个问题我之前准备过讲的是iOS12的UIScrollView在键盘弹出时contentOffset异常的问题。我会从复现、定位、猜测原因、验证猜想、修复、检查回归六个步骤来讲面试官感受到的是完整的排查链路意识。6.3 三面技术广度与软素质的结合三面通常是部门负责人或总监技术深度反而不一定特别深但会考察候选人对行业趋势的认知、沟通表达的条理性、以及价值观是否匹配。我三面时被问到“如果产品经理让你做一个你觉得技术上不合理的需求你会怎么处理”这个问题考察的是协作能力和专业判断力。我当时的回答是先了解产品诉求背后的目标再判断技术方案是否真的不可行如果只是实现成本高可以给出折中方案比如先上线一个简化版再逐步优化而不是直接拒绝。6.4 HR面就快拿offer了也别太飘如果进入HR面说明技术面基本通过了。HR主要考察稳定性、主动性、沟通能力。常见问题包括“base城市选择”“有没有其他offer”“对新人的培养期待”等。这里要注意的是不要表现出“我手里已经有A厂offer了你们不给我就亏了”的傲慢也不要表现得完全没有其他选择容易显得没有市场竞争力。客观坦诚地说明现状即可。7. 模拟真题自测这批面试题的答案对照表下面是我从2018校招iOS方向第三批记忆里整理出来的高频真题对照表方便大家自测。每道题我都给出了标准答案要点但不是唯一答案关键是要能讲清楚“为什么”。题目答案要点ARC下block属性为什么用copy保证block从栈拷贝到堆避免栈block失效语义上明确“我要一份独立的block”weak变量自动置nil的实现Runtime维护全局SideTable注册weak引用到weak_entry_t对象释放时遍历弱引用表把地址指向nilGCD死锁的成因当前线程同步等待一个需要当前线程空闲才能执行的任务形成互相等待UIButton和UIImageView的点击区别UIButton有target-action机制UIImageView默认userInteractionEnabled为NO无法接受触摸事件http和https的区别https在http和tcp之间加一层TLS/SSL提供加密、完整性校验和身份认证iOS中如何检测循环引用Instruments的Leaks/Allocationsdealloc是否调用_objc_autoreleasePoolPrint辅助观察链表反转递归写法的base casehead nil多线程中的数据竞争如何解决加锁NSLock、synchronized、串行队列屏障、原子操作避免多线程同时写同一资源Safe Area的作用解决刘海屏、圆角屏下视图被系统控件遮挡问题用safeAreaLayoutGuide约束布局为什么主线程要跑Runloop主线程需要持续监听并处理系统事件触摸、时钟、网络回调Runloop是其事件分发循环这10道题如果都能在30秒内组织好回答思路说明基础是过关的。如果有些题看完还是一头雾水建议回去翻翻《Objective-C高级编程》里关于ARC和Block的章节再配合Apple官方文档读一遍Runloop的说明。8. 写在最后几年之后再回看这批面试题2024年再回看2018年这批iOS校招题我的感觉是核心知识栈其实没有变变的是面试官提问的方式和侧重点。当年问的是“Runloop有哪些模式”今天可能问的是“Swift Concurrency里的Task和MainActor如何对应到线程和队列”当年问的是“KVO的原理”今天可能问的是“Swift的观察者模式和Combine框架怎么选”。但底层的考察逻辑始终是那条你能不能把一个机制讲清楚讲清楚它的输入、输出、内部流程和边界条件。如果你正在准备iOS校招面试我的建议是别只顾着刷面经面经只能帮你建立“考什么”的认知地图真正的分水岭在于你能不能把每个知识点从“面试官想听的答案”变成“自己真的理解并能推导的结论”。我的做法是每学一个机制都尝试用手画一遍它的流程图然后用口语复述给身边的朋友听。如果对方不用iOS也能听懂你在讲什么说明你真的掌握了。最后再分享一个面试中的小技巧回答技术问题时尽量用“先结论后展开”的结构。比如面试官问“KVO怎么实现的”你第一句话就说“KVO通过runtime动态生成子类并重写setter在setter里发通知”然后再展开细节。这样的回答节奏会让面试官觉得你思路清晰也为后续的追问留下了余地。这一点在我后来的工作评审和带人经历中也反复验证了它的价值。