☰
iOS秋招笔试题深度拆解:从内存管理到架构设计考点全解析
2026/9/27 11:48:51 网站建设 项目流程

“货拉拉2018秋招iOS工程师笔试题卷二(A)”——光看这个标题,很多准备跳槽的iOS开发可能心里会咯噔一下。货运物流场景下的App开发,和一般互联网公司的业务系统相比,对实时性、地图交互、弱网处理的要求高出一大截。这套卷子虽然是2018年的秋招题,但iOS开发的核心知识点演进速度并没有想象中那么快,内存管理、多线程、网络层、架构设计这些根上的东西,放到今天依然是面试考察的重点。对于正在准备iOS面试的初级工程师,或者想从业务开发转向基础架构方向的人来说,拆解这套题背后的考点,比单纯刷题更有价值。

这篇文章不打算一道题一道题地给标准答案,那样没意义。我更想做的,是从这套笔试卷子映射出的考察逻辑出发,把iOS开发面试中那些高频、深入、容易翻车的知识点摊开揉碎,讲清楚原理,也讲清楚面试官到底在考察什么。

1. 物流货运App的iOS端,到底在做什么技术活

看这张卷子之前,建议先理解货拉拉的业务场景。货运匹配平台的核心链路是:用户下单、司机接单、货物运输、在线支付、评价售后。这条链路里,iOS端承担的角色远不止一个展示页面那么简单。

  • 地图与定位:全程轨迹追踪、司机位置实时上报、围栏计算,这些功能极度依赖地图SDK和系统定位服务的深度结合。
  • IM与消息推送:用户和司机的实时沟通、订单状态变更通知,需要WebSocket长连接和APNs推送的双通道保障。
  • 弱网容错:司机在行驶途中经常经过地下停车场、隧道、偏远路段,网络状况极差,App必须做好请求重试、数据缓存、离线包管理。
  • 性能监控:货运场景下,司机端长期在前台运行,耗电、内存、CPU占用都是关键指标,HomeGuard式的后台保活策略虽然敏感,但合理的前台性能优化是基本功。

理解了这层业务背景,再看笔试题,你会发现它考察的方向其实和业务高度耦合。面试官想找的不是一个只会写UI的工程师,而是一个能在这个复杂场景下独立解决问题的人。

所以这套卷子的知识点分布,基本围绕语言特性、并发处理、网络知识、架构设计和实战经验这几个维度展开。这也是我在带团队面试时最关注的能力模型:基础扎不扎实,决定了你在生产环境遇到诡异问题时有没有排查方向。

2. 语言基础考点:OC特性与内存管理的底层逻辑

2.1 属性关键字不只是“背答案”那么简单

笔试卷子里几乎必考的一类题,就是@property的修饰词选择。atomic和nonatomic的区别、assign和weak的区别、copy和strong的区别,看起来是送分题,但深入问下去,很多人会露馅。

我见过不少简历上写着“精通iOS开发”的候选人,问到atomic时只会说“线程安全”,再往深问一层“它保证的是什么样的安全”就答不上来了。

atomic的原理是在生成getter/setter时加自旋锁,保证的是属性的读写操作原子性,也就是你读到的值要么是旧值要么是新值,不会读到一个中间状态。但它不能保证对象内部属性的线程安全。比如一个NSMutableArray的count和objectAtIndex:操作,即使属性声明为atomic,多线程同时读写这个数组依然会崩溃。真正的解决方案是使用pthread_rwlock或者dispatch_queue做数据同步。

copy和strong的选择也是高频考点。为什么NSString类型的属性要用copy?因为外部可能传入一个NSMutableString,如果属性是strong,这个可变字符串后续被修改,属性指向的对象也跟着变了,就会出现数据错乱。copy会生成一个不可变副本,把属性与外部对象隔离。同样的道理适用于NSArray和NSDictionary。这里有个小坑,自定义对象实现了NSCopying协议时,copy修饰符要求这个对象必须正确实现copyWithZone:,否则运行时会崩溃。

2.2 内存管理:从Retain Count到AutoRelease

苹果的内存管理从MRC时代演进到ARC时代,底层逻辑其实是统一的。ARC在编译期帮我们插入了合适的retain、release、autorelease调用,运行时还是靠引用计数在维持对象生命周期。

面试里经典的考察点是循环引用。常见的出题场景:Block里使用self、NSTimer的target强引用、Delegate的持有关系写错方向。很多人知道Block里用__weak打破循环,但有一个隐蔽场景容易忽略——Block被对象强持有,Block内部又捕获了这个对象的成员变量,即使你用了__weak typeof(self) weakSelf = self;,如果Block内部访问的是_ivar(成员变量),系统会直接强引用self,weakSelf失效。

// 错误写法:即使有weakSelf,_name访问仍会强引用self __weak typeof(self) weakSelf = self; self.block = ^{ NSLog(@"%@", _name); // 显式访问成员变量,weakSelf形同虚设 }; // 正确写法:先通过weakSelf拿到self,再访问属性 __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf) { NSLog(@"%@", strongSelf.name); } };

这个__strong修饰符的使用也是加分项。它防止在Block执行过程中,对象在中间某个时刻被提前释放,保证了执行期间对象的存活。

2.3 Category和Extension的底层实现差异

分类是OC中一个极具特色的机制。Category可以给现有类添加方法,但不能直接添加成员变量。原因是成员变量对应的是类的实例变量布局,而分类在运行时是合并进类的方法列表里的,并不参与class_ivar的布局调整。

面试题里常常问“为什么Category不能添加属性”,更准确的说法是“Category可以声明@property,但不会自动生成成员变量和getter/setter实现”。如果用objc_setAssociatedObject和objc_getAssociatedObject手动实现关联对象,就能曲线达成“给分类添加属性”的效果。

关联对象的使用有几个注意事项:

  • objc_setAssociatedObject的key需要是一个静态指针常量,不能直接用字符串字面量,否则多个地方用相同的key会互相覆盖。
  • 关联对象释放时机是被关联对象dealloc时,如果关联策略用了OBJC_ASSOCIATION_RETAIN_NONATOMIC,系统会负责释放关联对象,但要注意避免在对象释放后被其他线程访问。
  • 关联对象不能滥用,它走的是全局映射表,频繁使用会影响性能。
// 给UIView添加一个业务标识属性 static char kBusinessKey; @implementation UIView (Business) - (NSString *)businessKey { return objc_getAssociatedObject(self, &kBusinessKey); } - (void)setBusinessKey:(NSString *)businessKey { objc_setAssociatedObject(self, &kBusinessKey, businessKey, OBJC_ASSOCIATION_COPY_NONATOMIC); } @end

3. 多线程与并发:面试必考的GCD深水区

3.1 队列与任务的组合逻辑

GCD的核心概念是队列和任务。队列分串行和并发,任务分同步和异步,组合出来就有四种情况。很多面试题喜欢问“输出顺序是什么”,考察的就是对这四种组合的理解。

最经典的坑是dispatch_sync和dispatch_get_main_queue的组合。在主线程上执行下面的代码:

dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"Hello"); });

这行代码会直接死锁。原因很简单:主线程正在执行dispatch_sync,同步任务需要立即执行,但执行这个任务需要主线程,而主线程被dispatch_sync占住了,任务永远没有机会执行。

类似的死锁场景还有递归锁的使用不当。比如在串行队列里用dispatch_sync向同一个队列提交任务,一样会死锁。判断一个GCD调用会不会死锁,核心思路是:当前任务所在的队列,是否和提交任务的队列是同一个队列,并且当前任务用的是同步API。理解了这一点,大部分死锁问题就能规避。

3.2 栅栏方法与读写锁的取舍

dispatch_barrier_async是处理多读单写场景的经典方案。读操作可以并发,写操作必须独占。用栅栏方法实现,能保证写入任务提交之前的所有读任务完成,完成之后再执行读任务。

- (id)readDataForKey:(NSString *)key { __block id result = nil; dispatch_sync(self.concurrentQueue, ^{ result = [self.dictionary objectForKey:key]; }); return result; } - (void)writeData:(id)data forKey:(NSString *)key { dispatch_barrier_async(self.concurrentQueue, ^{ [self.dictionary setObject:data forKey:key]; }); }

这里面有两个细节值得注意:

  • 并发队列不要用dispatch_get_global_queue,因为全局队列会被其他模块共用,栅栏方法就失去意义了。
  • dispatch_barrier_sync和dispatch_barrier_async的区别在于是否阻塞当前线程。写操作如果不需要立即获取结果,用async是更好的选择,不会阻塞UI线程。

不过,GCD的栅栏实现也有局限。它只适用于你自己创建的并发队列,如果写操作的频率很高,性能会下降。更灵活的方案是使用pthread_rwlock_t,读锁共享、写锁独占,在线程竞争激烈的场景下吞吐量更高。

3.3 NSOperation的依赖与取消机制

NSOperationQueue是GCD的更高层封装。笔试题里常问它和GCD的区别,有一种说法是“NSOperationQueue是GCD的封装”,这个表述不太准确。NSOperation和NSOperationQueue是基于GCD实现的,但额外提供了依赖关系、最大并发数控制、暂停恢复、取消操作等能力。

依赖关系是面试的高频考察点。假设有三个网络请求:A获取用户信息,B获取用户订单列表,C需要同时使用A和B的结果做展示,那么C应该依赖A和B都完成。实现方式是:

NSOperation *aOp = [NSBlockOperation blockOperationWithBlock:^{ // 请求A }]; NSOperation *bOp = [NSBlockOperation blockOperationWithBlock:^{ // 请求B }]; NSOperation *cOp = [NSBlockOperation blockOperationWithBlock:^{ // 并发执行完成后,处理A和B的结果 }]; [cOp addDependency:aOp]; [cOp addDependency:bOp]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperations:@[aOp, bOp, cOp] waitUntilFinished:NO];

有一个容易忽略的问题:cOp执行时,A和B的任务确实完成了,但数据如何传递给C?如果直接使用共享的可变字典,需要保证线程安全。更优雅的方案是使用NSOperation的子类,把上一个操作的输出作为当前操作的输入,通过自定义数据属性来传递,配合completionBlock做最终处理。

取消操作也是容易踩坑的地方。调用[operation cancel]并不会停止正在执行的代码,它只是在operation.isCancelled返回YES而已。规范的做法是在执行的Block里手动检查取消状态:

for (int i = 0; i < 10000; i++) { if (op.isCancelled) { break; } // 每执行一段耗时操作,检查一次取消标记 }

如果忽略这个检查,取消操作就是纸上谈兵,任务依然会执行完。

4. 网络层与数据安全:每一层都在考察什么

4.1 TCP三次握手和四次挥手,不能只背口诀

笔试题几乎必考TCP连接管理。但单纯背出“三次握手、四次挥手”顺序,最多拿一半分。面试官通常会追问:为什么是三次而不是两次?为什么挥手是四次而不是三次?

三次握手的核心目的是同步序列号,确保双方都知道对方的初始序列号。如果只有两次握手,服务端无法确认客户端是否收到了自己的SYN+ACK报文,可能导致半连接状态。四次挥手多出来的那一次,是因为TCP的支持全双工通信,每一方的FIN都只能关闭自己这一侧的传输方向,所以需要双方各发一次FIN和ACK。

还有一个容易混淆的考点是TIME_WAIT状态。主动关闭连接的一方会进入TIME_WAIT状态,持续2MSL(Maximum Segment Lifetime,报文最大生存时间)。这个状态存在的理由有二:

  • 防止最后一个ACK丢失,导致被动关闭方重发FIN,而主动方已经关闭了,收不到重发的FIN。
  • 确保本次连接的所有报文在网络中消失,避免复用相同端口和IP的新连接收到旧数据。

TIME_WAIT数量过多的问题在生产环境很常见,尤其是高并发短连接场景。解决思路包括使用长连接、开启SO_REUSEADDR、调整内核参数net.ipv4.tcp_tw_reuse等。但要注意,tcp_tw_reuse只对客户端生效,服务端不能依赖它。

4.2 HTTPS握手过程中,客户端到底校验了什么

HTTPS的握手过程是面试的高频题。完整的TLS握手涉及证书验证、密钥交换、对称加密协商三个阶段。对于iOS开发来说,重点不只是握手的流程,还有客户端如何校验证书。

证书校验分为几个层级:

  • 证书链校验:从叶子证书开始,逐级向上验证到根证书,确保证书链完整。
  • 有效期校验:SecTrustEvaluateWithError返回true,代表证书在有效期内。
  • 域名校验:证书里的CN或Subject Alternative Name需要和请求的域名匹配。
  • OCSP吊销校验:可选,但推荐做,用来确保证书没有被吊销。

iOS 13之后,NSURLSession默认要求ATS(App Transport Security)通过,也就是必须使用HTTPS。开发阶段的临时豁免NSAllowsArbitraryLoads可以打开,但上架审核时如果被检测到,有被拒的风险。我的建议是,开发阶段用NSExceptionDomains给特定域名开白名单,而不是全局禁用ATS。

有一类面试题会问:AFNetworking的证书校验是怎么实现的?答案是它封装了AFSecurityPolicy,提供三种模式:无校验、白名单校验、公钥校验。生产环境推荐公钥校验模式,这样即使证书过期或者更换了证书(只要公钥不变),App依然能够正常工作。但公钥校验的缺点是私钥泄露时整个App的通信防护都失效。

4.3 DNS解析的坑:从HTTPDNS到Happy Eyeballs

物流货运App对网络延迟极度敏感。DNS解析是连接的起点,但它也经常成为性能瓶颈。传统DNS有两个问题:

  • 解析慢:UDP的53端口如果被运营商劫持或者缓存污染,可能导致连接失败。
  • 解析结果不准确:CDN调度需要判断用户所在的运营商网络,但有些DNS服务器返回的IP不是最优节点。

所以现在大厂的应用普遍使用HTTPDNS方案。iOS端接入HTTPDNS,通常是在NSURLProtocol里拦截请求,把URL的域名替换成HTTPDNS返回的IP,同时在Host头字段保留原始域名。这样既绕过了本地DNS,又让服务端能够识别域名。

这个方案在面试中也经常出现,关键是理解它的实现原理,而不是只会调SDK。HTTPDNS要做到精确替换,需要处理HTTPS下的证书校验问题,因为服务端的证书是和域名绑定的,IP直连时验证证书会失败。解决方案是自定义SecTrust的校验策略,用原始的host去校验证书。

5. iOS架构设计:笔试中的加分题和团队协作分水岭

5.1 MVC、MVVM、MVP到底差在哪

架构设计题几乎是高级岗位的必考题。笔试卷子里可能会出现一个场景描述,让你设计一个订单详情页面的架构,考察的就是你对几种架构模式的理解和取舍。

MVC(Model-View-Controller)在iOS中是最经典的架构,但实际开发中容易退化成Massive View Controller。原因在于Controller承载了过多的职责:网络请求、数据处理、UI更新、事件响应、页面跳转,全部堆在一起,一个文件动辄上千行。

MVVM的核心改进是把业务逻辑和视图状态绑定拆到了ViewModel层。View只负责展示和发送交互事件,ViewModel持有模型数据并暴露给View可绑定的属性,View和ViewModel之间通过Block、Delegate或响应式框架(如RAC、Combine)通信。这样做的好处是ViewModel不依赖UIKit,可以纯单元测试。

但MVVM也有坑。使用RAC或Combine时,数据绑定链如果处理不好,会产生调试困难的问题。我的经验是:不需要追求100%的绑定,在关键数据流上做绑定就行,复杂的业务逻辑还是用显式的方法调用更清晰。

架构选型没有银弹,面试官想听的是你能针对具体场景分析优劣,而不是背出某个架构的定义。一个合格的回答应该包含:

  • 说出当前场景的核心痛点(比如页面逻辑复杂、多人协作容易冲突、测试覆盖难)。
  • 说明你选择的架构如何解决这些痛点。
  • 承认这个架构的局限性,以及你用什么手段弥补。

5.2 组件化改造:不是为了炫技,是为了解决真实问题

谈到iOS架构,组件化是一个绕不开的话题。货运业务经过多年迭代,App体积变大、模块增多,业务线之间的代码耦合越来越严重,所以组件化改造在笔试和面试中频繁出现。

组件化常用方案有CocoaPods私有库和Lotusoot、CTMediator这类中间件方案。中间件方案的核心是通过运行时发现服务、调用服务,解除业务模块之间的直接依赖。比如模块A要调用模块B的某个功能,A不直接依赖B的头文件,而是通过URL Router或者Protocol匹配的方式,找到可以处理这个调用的模块。

这套机制在解耦业务线的同时,也引入了一些问题:

  • 动态发现机制依赖编译期注册表,调试时如果注册表没刷新,会出现“服务找不到”的诡异bug。
  • 隐式的调用方式,让静态分析和代码跳转变得困难。
  • 参数类型不安全,所有参数以字典形式传递,编译期无法发现类型错误。

所以我的建议是:小团队、小规模项目不需要一上来就组件化,先把模块的接口设计清楚,控制好依赖方向,比引入复杂的中间件更有效。中大型项目做组件化的目的,一定是解决真实存在的协作效率问题,而不是因为别人都在做。

5.3 启动优化和卡顿优化,笔试里的实战题

性能优化是笔试卷里最具实战色彩的部分。启动时间、FPS流畅度、内存占用,每一个指标背后都有一整套优化方法论。

启动优化有两个阶段:pre-main阶段和post-main阶段。pre-main阶段主要包括dyld加载动态库、图片资源解压、+load方法执行、C++全局对象构造。启动优化措施包括:

  • 减少动态库数量,将多个Pod库合并成framework或者静态库。
  • 清理用不到的+load方法,特别是组件化后各模块在+load里注册表的逻辑,可以改为懒加载注册。
  • 把didFinishLaunchingWithOptions里的非关键业务延后到首帧渲染完成后执行。
  • 图片资源尽量使用[UIImage imageNamed:]之外的加载方式,避免主线程解码。

卡顿优化的根因通常是主线程执行了耗时操作。排查手段包括使用Time Profiler、Instruments做采样分析,或者通过CADisplayLink监控FPS,把卡顿的堆栈上报到平台。常见的卡顿源头无非是:同步网络请求、大量正则匹配、字符串拼接、图片解码、复杂布局计算(layoutSubviews、constraintWithVisualFormat)。每一个都有对应的优化手段,但归根结底的原则是——能异步就异步,能懒加载就懒加载,能缓存就缓存。

6. 从笔试题到技术终面:那些容易被忽略的隐性考察点

6.1 考察的是能力模型,不是答案本身

很多人以为笔试就是做题,背熟知识点就够了。但真正的行家透过笔试题,看的是你解决问题时的思维路径。

比如一道关于TableView卡顿优化的题,候选人的回答可能是“减少cell高度计算、异步加载图片、复用cell”,这些是标准答案,能拿及格分。但高分答案是进一步说清楚:“cell高度的计算逻辑在何时执行,有没有可能预计算;图片解码是否已经放到子线程,解码结果有没有缓存;快速滑动时是否复用了cell导致图片错乱,是否有根据indexPath做校验”。后者体现的是你在真实项目中踩过坑,才能真正关心这些细节。

笔试卷里的开放题,比如“设计一个日志系统”“说一个你印象最深的线上bug”,其实都是给候选人展示自己能力模型的机会。我的建议是,写这类题目时不要泛泛而谈,采用“背景-动作-结果”的结构:这个问题的业务背景是什么,我做了什么分析,最终怎么解决的,效果如何。这个结构也适用于面试时的自我介绍和项目介绍。

6.2 竞态条件和线程安全:考试套壳的深层考点

与iOS直接相关的并发线上问题,有一个高频场景是竞态条件。多线程同时读写同一个可变字典、同一个计数器、同一个网络请求回调,都容易产生难以复现的闪退。笔试卷子里经常用一个看似简单的例子来考察:

@property (nonatomic, strong) NSMutableDictionary *cache; // 线程A self.cache[key] = valueA; // 线程B id value = self.cache[otherKey];

这段代码在nonatomic修饰下,两个线程同时操作字典,会出现两种情况:读取到未完整的值、或者触发EXC_BAD_ACCESS。即使把属性改成atomic,这里的问题依然存在,因为atomic只保证属性的setter/getter原子性,并不保证NSMutableDictionary的底层容器线程安全。

正确做法是把字典接在并发队列后面,用读写锁保护。这是面试中的送分题,但也是生产环境的高频事故,值得重点记忆。

6.3 手写代码的常见坑和解题套路

大部分iOS笔试包含手写代码环节。核心类型是算法题或手工实现API。这里总结几个容易翻车的点:

  • 边界条件:数组越界、空数组、长度为1的数组。
  • 变量命名:不要用a、b、tmp这样的命名,用有业务含义的名字。
  • 时间复杂度:别写出O(n^2)的解法还不自知。
  • 内存管理:手写Block或闭包时,记得处理循环引用。
  • 考虑线程安全:如果题目明确说多线程访问,补充锁或队列保护。

一个常见的解题思路模板:先想清楚输入输出,再写核心逻辑,最后补边界判断。写完之后主动说出时间和空间复杂度,以及潜在优化空间。这些细节在人工阅卷时非常加分。

7. 实战复盘:一份2018真题卷里的iOS高频知识点速查表

既然这是一篇围绕笔试题的深度拆解文章,最后整理一份高频考点速查表,方便备考的朋友按图索骥。下面这些考点,是我根据多套iOS笔试题和多年面试经验归纳出来的,涵盖范围比较全面。

知识点高频考察形式容易翻车点推荐解决思路
属性修饰符选错copy/strong/weak可变对象传入后数据被篡改字符串、数组、字典用copy
循环引用Block、NSTimer、Delegate访问成员变量导致weakSelf失效__weak + __strong组合
关联对象分类添加属性key值冲突使用静态指针常量做key
GCD死锁dispatch_sync提交到主队列代码直接卡死判断队列关系
读写锁多线程读写数据并发队列用全局队列自建并发队列+barrier
NSOperation取消调用cancel后任务仍执行未检查isCancelled循环内手动检查取消标记
TCP状态TIME_WAIT和CLOSE_WAIT只背四次挥手顺序结合场景理解
HTTPS证书校验ATS、证书链、公钥校验忽略域名校验注意自定义SecTrust
HTTPDNS域名替换和Host头HTTPS证书验证失败自定义证书校验策略
架构模式MVC/MVVM选型Controller膨胀按业务复杂度合理选型
启动优化pre-main耗时+load方法过多懒加载注册
卡顿优化主线程耗时操作图片解码在主线程异步解码+缓存

这份速查表的核心价值,是帮你快速定位自己的薄弱环节。每一个知识点背后,都对应着一个真实业务场景。你能把场景讲清楚,比单纯记住答案更有说服力。

如果你正在准备iOS面试,我的建议是:不要只刷题,把每个知识点放到一个具体的业务场景里,想清楚“这个技术到底解决什么问题、不用的后果是什么”。这样做的好处是双重的,面试时能言之有物,入职后也能更快地上手解决实际问题。

我个人带团队的经验是,笔试能反映候选人的知识广度,但真正决定能不能干活的是看他遇到具体问题时的排查思路。比如一个线上卡顿问题,他是直接猜、肉眼盯代码,还是用Instruments采样、用火焰图定位,这两种思维方式在笔试开放题中会体现得非常明显。写这套卷子的解题反思时,不妨多问自己几个“为什么”,把每个考点背后的原理吃透,这样不管面试官从哪个角度切入,你都能接得上话。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询