iOS秋招笔试复盘:从内存管理到RunLoop的核心考点解析 2023年秋招季过去有一阵子了但小满秋招iOS研发岗第一批笔试那道卷子我到现在还能凭记忆复盘出大半。说实话这份卷子跟市面上很多“题库式”校招笔试不一样题量不算大但每一道题都像是有意在试探你对iOS底层的理解不只是看你背没背过API。我整理了一份完整复盘把出题思路、核心考点、编程题的解法以及我踩过的坑都写出来希望能给后面准备秋招、想冲iOS研发岗的同学一点参考。1. 笔试整体设计与考察思路1.1 出题风格与考察目标先说整体感受。整张卷子大概60分钟一共四类题型15道选择、5道填空、3道简答、2道编程总分100分。从出题风格看它没有像很多公司那样靠海量题目拼记忆而是故意把题出的“活”一点让你不能用背题的方式混过去。比如它不会直接问“UITableView的复用机制是什么”而是换了个场景问你“为什么复用池中取出的cell有时候需要手动重置状态”这种问法就需要你真正理解复用机制背后的数据流和状态管理光背概念是答不透的。从考察目标来看这份卷子很明显是在筛两类人。一类是只会调API、照着demo敲过几遍就敢投简历的另一类是真正理解iOS运行时机制、遇到问题有自己的排查思路的。选择题部分还相对友好但简答题和编程题只要功力不够基本就露馅了。它的核心逻辑就是基础题保证及格线区分度全部放在后面的大题上你能不能进下一轮起决定作用的还是后40分。1.2 题型分布与分值参考题型题量分值占比考察重点单选题12题24%OC语言特性、KVC/KVO、内存管理多选题3题6%多线程、RunLoop、事件传递填空题5题20%细节记忆、API使用场景简答题3题20%机制原理、场景分析编程题2题30%代码功底、架构设计这种分布非常典型也很有参考价值。基础题占大头意味着你只要平时开发不划水选择题填空题拿到80%以上的正确率是完全可能的。但真正拉开差距的是后面的简答和编程题尤其是编程题几乎直接决定你能不能进面试。如果你练过一段时间力扣但平时没怎么手写过iOS相关代码那最后40分就很危险因为它的编程题不是算法题而是工程题考的是你在iOS环境下组织代码的能力。2. 核心知识点拆解与答题思路2.1 OC与Swift的基础功底考察笔试里OC的题目占了半壁江山这并不意外。即便现在Swift越来越主流大厂的存量代码大部分还是OC写的新项目即使是Swift底层也逃不开OC runtime那一套。所以OC基础牢不牢基本能判断一个人能不能直接投入项目。有一道选择题我记得很清楚题干是NSArray *array [a, b]; NSMutableArray *mArray [array mutableCopy];问mArray的类型以及后续能不能调用addObject:。这道题其实考的是深拷贝和浅拷贝以及可变拷贝的返回类型。很多人只记结论“mutableCopy出来的一定是可变对象”却没认真想过如果源对象是NSArraymutableCopy返回的是一个独立的NSMutableArray实例不是原来的引用。题目这么一出就能筛掉那些只背结论不看原理的人。还有一道关于block的题目也很有代表性__block NSInteger count 0; void (^block)(void) ^{ count; }; count; block();问count最终的值。答案是2。这里考的是__block变量的捕获机制以及block执行时机。如果你对__block的理解只停留在“用了就能在block里改外部变量”那这题很容易错。它其实是在问block捕获的是变量本身还是捕获的值拷贝在ARC下__block变量会被包装在一个结构体中block持有这个结构体的引用所以block执行时能看到外部对count的修改反过来也一样。能答到这个层面才算真的理解。Swift方面也考了一两道基础题比如结构体和类的区别问的是“以下哪种类型在赋值时是值拷贝”。这题只要复习过都能答对。但如果你在开发中根本没写过Swift后面的简答题会让你解释Swift的Optional底层实现很多人就只能写个“可选类型要么有值要么为nil”拿不到什么分。我的建议是即使你投的是OC为主的项目Swift基本概念也必须过关因为笔试出题人默认你两者都有所涉猎。2.2 内存管理与引用计数的底层逻辑内存管理是iOS笔试的重头戏这批笔试在内存管理上至少出了4道题分布很密集。第一道是ARC下strong、weak、copy、assign修饰符的选择。这属于基础中的基础但题目设置了一个小陷阱对于block属性到底应该用copy还是strong。在ARC下这两者效果其实等同因为编译器会自动处理block的拷贝但在MRC时代必须用copy否则block可能在栈上被提前释放。我知道很多人会直接选copy但如果能在旁边标注“ARC下strong也行但保留copy是历史习惯也更安全”这个细节会成为加分项。第二道是autoreleasepool的优化场景问你“在什么样的场景下需要手动创建自动释放池”。最常见的回答是“在大量产生临时对象的循环内”但出题人还希望你补充为什么。原因是自动释放池的释放时机是在当前RunLoop循环结束的时候如果循环体内创建了大量autorelease对象它们会一直堆积到RunLoop结束才统一释放导致内存峰值过高甚至出现内存警告。如果在循环体内部加一个autoreleasepool就能让这些临时对象在每轮迭代结束时立即释放内存曲线会平缓很多。第三道题比较意外考的是dealloc里面可以做什么、不能做什么。很多人知道不能在dealloc里访问self.property但不知道编译器的警告到底是在提醒什么——因为对象正处于销毁过程中属性的getter/setter可能访问到已部分释放的成员变量。这道题能答对的人不多但答对了就很有区分度。第四道是NSTimer和循环引用的问题。卷子里给了一个场景控制器里用NSTimer做倒计时页面pop之后计时器还在跑导致控制器和timer互相持有内存泄漏。这题的常规解法是用一个中间代理对象WeakProxy打破循环或者在iOS 12之后使用带block的timer方式并注意在block里弱引用self。光写“在viewWillDisappear里invalidate”是不够的因为invalidate的时机如果不对仍然会有窗口期泄漏。2.3 多线程与并发编程多线程这个模块卷子考了GCD和NSOperation的对比这个几乎是iOS面试必考点。它真正想知道的不是“你会不会用”而是“你在什么场景下选谁”。GCD适合处理简单并发任务比如异步下载图片后回主线程刷新UI直接dispatch_async到全局队列再回到主队列更新即可。NSOperationQueue则更适合需要任务依赖、取消、最大并发数控制的场景因为它的API层面直接支持这些能力GCD用起来要手动组合相对麻烦。有一道简答题是现在有A、B、C三个异步任务要在全部完成后执行D写出至少三种方案。我当时写了三种使用dispatch_groupenter/leave配对配合dispatch_group_notify执行D。使用NSOperation设置B和C依赖AD依赖B和C然后所有operation加入同一个queue。使用DispatchSemaphore信号量初始为0每个任务完成时signalD执行前wait三次。这种题其实没有唯一标准答案关键是看你对并发工具的掌握面。如果只答出一个dispatch_group也能得分但三种都能列出来面试官对你的评价会明显上一个档次。而且你要注意方案不是写出来就完了还得说明为什么这个方案能保证“全部完成”这里考的是对dispatch_group的enter/leave机制、NSOperation依赖关系、信号量计数这三套底层逻辑的理解。还有一道选择题是问“以下哪些方式可以安全地在主线程更新UI”选项包括dispatch_async(dispatch_get_main_queue())、performSelectorOnMainThread:、NSOperationQueue的addOperationWithBlock、直接在其他线程调用updateUI。实际上前三个都能保证最终在主线程执行第四个是不安全的所以正确选项是三个。我当时对NSOperationQueue的实现不够熟悉只选了前两个白白丢了一道多选。这个教训是多线程的选择题别只看结果还要想明白每一种方式底层是怎么把任务投递到主线程的。2.4 RunLoop与视图渲染机制RunLoop考了一道选择题和一道简答题。选择题是问RunLoop有哪几种mode以及默认mode是什么。这题对平时搞过滚动监听或者读过高性能滚动方案的人来说不难kCFRunLoopDefaultMode和UITrackingRunLoopMode是核心还有GSEventReceiveRunLoopMode等不太常用的。简答题更有意思问的是为什么在滚动UIScrollView的时候NSTimer会暂停这个问题很经典答案的核心在于RunLoop模式切换。滚动过程中RunLoop会切换到UITrackingRunLoopMode而timer默认注册在kCFRunLoopDefaultMode里滚动期间默认模式的事件源不会被处理timer自然就暂停了。解决方案也简单把timer加到NSRunLoopCommonModes中或者改用dispatch_source_t来替代timer。视图渲染那块考了离屏渲染。题目问哪些情况会触发离屏渲染以及怎么优化。这题如果平时只做业务开发、不关注性能优化很可能答不完整。常见触发情况包括圆角加masksToBounds、阴影、mask遮罩、layer光栅化。优化手段通常是使用上下文绘制圆角、用贝塞尔曲线裁剪或者合理使用shouldRasterize并注意光栅化缓存的开销。这里有一个比较坑的认知误区是“所有圆角都会离屏渲染”其实单设cornerRadius不裁剪的情况下大部分场景不会触发离屏渲染只有cornerRadius和masksToBounds同时设置才会。这种细节出题人就是想看你有没有真正debug过渲染流程。3. 实操题与编程题详解3.1 手写单例与线程安全编程题第一道很经典在ARC下写出一个线程安全的单例类。这道题看起来是送分实际上很多人会写错。如果你直接写成 (instancetype)sharedInstance { static MyManager *instance nil; if (instance nil) { instance [[MyManager alloc] init]; } return instance; }这在多线程环境下是不安全的并发调用可能创建出多个实例。正确做法是用dispatch_once (instancetype)sharedInstance { static MyManager *instance nil; static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ instance [[MyManager alloc] init]; }); return instance; }这算是标准答案。但如果你能在代码旁边多写一句“这里的dispatch_once底层通过原子性加锁保证即使多线程同时调用初始化代码也只会执行一次并且能确保内存可见性”这比只写代码的同学就高了一截。笔试考察的不只是代码能不能跑而是你是否清楚每行代码背后的保证是什么。再深一层如果把“线程安全”理解为不只是实例唯一还包括单例内部属性的读写安全那就需要加atomic修饰或者用并发队列保护。很多同学忽略了这一步但如果你能补充说明“单例的创建安全并不等于内部属性访问安全”面试官会觉得你踩过真实的坑不是一个只会背单例模板的候选人。3.2 手写UIScrollView轮播图第二道编程题是用UIScrollView实现一个自动循环滚动的轮播图并说明关键步骤。这道题给出了项目工程和部分基础代码需要你在指定位置补全。关键点有几个设置scrollView的contentSize为页宽乘以页面数量用UIPageControl显示当前页数用NSTimer实现自动滚动手动拖拽时暂停计时器停止拖拽后再恢复。如果只是逻辑补全大多数人能写出来但要想拿高分需要你在注释里写清楚设计原因。比如“NSTimer会强引用self必须弱化self否则控制器无法释放”、“在开始拖拽时invalidate旧timer并在停止拖拽时重新创建防止用户正在阅读时被自动切换打断”。另外还要求处理最后一张滑到第一张的循环效果常见做法是在首尾各加一页滚动到边界时无动画跳转。这个细节很多人会漏漏了之后轮播图滑动体验就很生硬面试官一眼就能看出你平时写没写过这类组件。当时我补全的部分大概是这样的核心思路- (void)startAutoScroll { if (_timer) { [_timer invalidate]; _timer nil; } __weak typeof(self) weakSelf self; _timer [NSTimer timerWithTimeInterval:3.0 repeats:YES block:^(NSTimer *timer) { [weakSelf autoScrollToNextPage]; }]; [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; }这一段代码里包含了好几个踩坑点包括弱引用避免循环、使用NSRunLoopCommonModes让timer在滚动模式下也能工作、timerWithTimeInterval的block方式替代target-action避免保留环。这些点单独拎出来任何一个都能当一道简答题。3.3 网络请求封装方案其实编程题还有一道选做很多人因为时间不够没动笔题目是设计一个网络请求的封装工具类要求支持GET/POST请求、超时设置、异步回调、请求取消。这题考的是架构能力和代码组织能力不是算法。我当时提供的核心方法思路是这样的 (NSURLSessionDataTask *)GET:(NSString *)url parameters:(NSDictionary *)parameters timeout:(NSTimeInterval)timeout success:(void (^)(id response))success failure:(void (^)(NSError *error))failure;如果你只是把苹果的NSURLSession封装一层当然也能用但这题的高分点在于“请求取消”和“生命周期绑定”。你需要在调用方页面销毁的时候统一取消尚未完成的请求避免网络回调回来时操作已经销毁的UI构件。更合理的做法是提供一个基于任务池的管理器控制器dealloc时自动cancel自己发起的请求。这已经不仅仅是一个工具类而是网络层与业务层生命周期解耦的设计问题。能考虑到这一层说明你真的处理过线上那种“页面关了还在弹窗”的bug。4. 常见问题与避坑经验4.1 笔试中踩过的坑我踩过最亏的一个坑是填空题第一题“请写出nil、Nil、NULL、NSNull的区别。”我写的是“nil是OC对象空指针Nil是类对象空指针NULL是C语言空指针NSNull是一个代表空值的对象。”从字面上看没错但事后复盘失分点在于我没有说明NSNull在集合类中的应用场景以及NULL和nil本质上都是(void *)0这个底层事实出题人想要的是那种“理解了指针的本质之后自然得出结论”的回答而不是四个名词解释。另一个坑是简答题里“weak属性为什么在对象释放后自动置为nil”当时我只写了“因为weak是弱引用不增加引用计数”这其实只回答了一半。笔试后查资料才知道weak变量会注册在SideTable的weak表中对象释放时会通过dealloc触发clearDeallocating遍历所有指向该对象的weak指针并置为nil然后从weak表中移除。能答到这一层才算是把weak的底层机制讲清楚。否则面试官看你简历写了“熟悉内存管理”追问两句就露怯了。还有一个印象很深的多选题就是前面提到的“哪些方式可以安全地在主线程更新UI”。我多选少选了一个选项教训是做题不要凭感觉要把每一种机制都拆到最底层去验证。比如performSelectorOnMainThread:其实是基于RunLoop的它会把selector打包成一个timer事件投递到主线程RunLoop这种方式在滚动模式下如果不指定mode也可能延迟执行。NSOperationQueue的addOperationWithBlock则依赖主队列本质是串行队列加主线程调度。多线程的坑往往不在API本身而在你对API所属的调度体系是否有全局认知。4.2 面试官视角的评分标准笔试结束后我找在里面的学长打听了评分逻辑他给我讲了几点很实在的信息。首先笔试不要求满分大概60分左右就能进面所以遇到不会的题千万别空着哪怕只写了几行关键代码比如dispatch_once、contentSize设置、timer的weakSelf都能拿到步骤分这在编程题里尤其明显。其次选择题和填空题是卷子的底线这部分不能砸。只要平时开发认真选择题正确率做到85%以上是应该的。简答题是分水岭能不能进面看的就是简答题的深度。很多人编程题写不完但简答题回答得有层次、有因果链照样能拿到面试机会。原因很简单批卷的人看的是“你会不会思考”不是“你有没有模板”。第三点比较反直觉编程题不要求一次写出完美可编译的代码。考官见过太多“背题机器”他们更愿意看到你写了注释、写了关键设计思路哪怕代码有部分逻辑漏洞也比空白卷强得多。我当时就有一道编程题没写完整但把两个关键方法的思路和调用关系写了上去最后依然进了下一轮。4.3 备考建议与资源顺序如果你目标是iOS研发岗时间有限的话我建议按照这个优先级准备iOS基础 网络 多线程 UI性能优化 架构设计。前面几项是笔试面试高频考点也是平时写业务代码最容易积累的部分后面的内容更有区分度但对于应届生来说能做到有初步认知就够了不用花大量时间硬啃。我反复推荐的资料有两份《Objective-C高级编程iOS与OS X多线程和内存管理》这本书虽然有点年头了但把引用计数、weak表、autorelease、block、GCD的核心机制讲得很透笔试里内存管理和多线程的题基本都逃不出这本书的框架另一份是官方文档的Memory Management Guide作为补充查漏。另外我强烈建议自己做实验验证比如你可以在工程里写一个测试页面在UIScrollView滚动的过程中打印RunLoop的currentMode直观感受模式切换这个比记任何博客笔记都有用。复盘完这份笔试我的整体体会是小满秋招这批题目并不刁钻但覆盖面很扎实能比较客观地反映出一个应届生对iOS开发的理解程度。就算最后没拿到offer认真把这套卷子从头到尾复盘一遍收获也远大于刷几十道力扣。最后再给一条实在的建议笔试的时候先把整张卷子扫一遍优先做你最有把握的部分编程题哪怕只写核心思路也一定不要空着因为步骤分是真的会给的。