货拉拉iOS秋招笔试复盘:从Runtime到组件化的核心考点解析
如果你经历过几场大厂的iOS笔试会发现大多数卷子其实都有规律可循前半场考语言基础和内存管理中间用Runtime和多线程穿插一些“底层原理解析”最后放一两道架构设计或性能优化的大题让真正做过项目的人有机会展现实战积累。货拉拉2018秋招iOS这套卷子卷一B走的也是这个路子但题目打磨得很细几乎每一道都长在真实业务痛点上了。我在复盘这套题的时候最大的感触是它不像很多公司那样喜欢出刁钻偏题而是把货拉拉业务里真正常碰的“网络传输”“地图轨迹”“并发请求”“离线同步”这些场景静悄悄塞进了各个考点。换句话说这套卷子考察的不是你背了多少API而是你有没有用iOS工程师的思维去解决过真实问题。下面我把这份卷子的考点逐题拆开讲把每道题背后的原理、坑点和答题思路都过一遍。不管你是在准备秋招还是想查漏补缺这份复盘都值得收藏。1. 这份卷子考什么货拉拉的iOS岗位到底想要什么样的人1.1 从业务反推考点货运物流场景下的iOS能力模型先讲一个很多人容易忽略的前提笔试题目不是凭空出的它是由业务目标倒推回来的。货拉拉的主业务是货运匹配和同城/跨城物流用户端用App下单司机端用App接单、导航、上传货物照片两个端都重度依赖地图、定位、实时通信和大文件传输。把业务场景翻译成iOS工程师的技术要求就是这样一张能力清单网络层能力司机在弱网环境下要能正常上报位置、上传货物图片涉及断点续传、超时重试、请求优先级管理。多线程并发能力多个定位点、多个图片、多个订单请求同时到达线程调度与线程安全是基本功。内存与性能优化能力地图页面大量标注点渲染、轨迹绘制稍不注意就会卡顿甚至内存暴涨。架构设计能力订单、支付、地图、IM等业务模块并存代码工程必须做模块化解耦。底层原理理解熟悉Runtime、RunLoop这些机制才能在碰到疑难Crash和复杂动态化需求时游刃有余。这套卷子并没有单独开一个“业务设计”板块但你会发现几乎每道题都能在货拉拉的业务里找到投影。这是一套很聪明的出题方式不考你“知不知道”考你“有没有思考过”。1.2 卷面结构与命中率最高的知识点分布根据我对这类笔试题的长期观察和记忆卷一B的典型结构大致是选择题填空题覆盖Objective-C语言特性和内存管理简答题集中在Runtime消息机制、GCD死锁原因、Block循环引用应用题和编程题则落在网络断点续传设计、TableView流畅度优化和组件化方案。下面的表格是我根据经验整理出的知识点分布和高频命中率方便你对照复习知识模块常见题型货拉拉业务映射命中率预估Objective-C特性属性、Block、Category选择、填空订单模块的数据模型与回调处理极高内存管理MRC/ARC、循环引用、weak实现选择、简答地图控件与网络回调中的对象持有极高Runtime消息发送、消息转发、Swizzling简答、填空埋点统计、AOP日志、动态化方案高多线程GCD、NSOperation、线程安全简答、编程并发请求、地图轨迹实时上传高网络层断点续传、缓存、Cookie应用设计大图上传、离线数据同步中高性能优化启动速度、列表卡顿、包体积简答、编程地图页流畅度、冷启动耗时中高架构设计组件化、MVVM、消息机制论述多业务模块解耦、跨端逻辑复用中等看到这个分布你应该能明白这套卷子的核心主线是“把iOS底层原理和真实业务场景做结合”。它不是让你背一个孤立的知识点而是希望看到你把知识点串起来讲清楚这么设计能解决什么问题。2. 语言基础与内存管理题想拿满分必须先把这些坑填平2.1 属性关键字不是背出五种就够了属性关键字几乎是每张iOS笔试试卷的固定开场。copy、strong、weak、assign、atomic、nonatomic大多数人都能背出来但真正拉开差距的是你能不能在一段具体代码里选对。一个典型考法是给出一个模型类比如司机信息里面有NSString类型的手机号、NSArray类型的常用路线、NSNumber类型的评分问你这些属性分别应该用什么关键字修饰。最稳的答法是这样对于NSString、NSArray、NSDictionary这类有可变子类的对象一律用copy。原因很直接外部可能传进来一个NSMutableString如果你用strong持有外部后续修改这个可变字符串你的属性值也会跟着变订单列表里的司机手机号突然变了这种bug极难排查。对于delegate、NSTimer、CADisplayLink这类有可能产生循环引用的对象用weak。对于基本数据类型NSInteger、CGFloat、BOOL用assign。注意assign不能用于对象类型否则对象释放后指针悬空再访问就是野指针崩溃。我在帮人review代码时真的见过这样的问题有人给一个NSString属性用了strong结果在列表页复用时数据源里的可变字符串被后续逻辑篡改整个界面的手机号全部变成同一个。这种问题不是运行时必现所以更难定位。笔试里把这个知识点拿出来就是看你有没有被真实世界的bug教育过。2.2 Block循环引用的三个高频场景循环引用题基本会围绕Block来出。核心原理一句话就能说清当A持有BB的Block里又强引用了AA和B就形成一个环ARC环境下谁都无法释放。但笔试不会只让你背概念它会给你三段代码让你判断会不会内存泄漏。第一段是self持有blockblock内部使用self调用方法。这必泄漏。用__weak typeof(self) weakSelf self包一层即可。第二段是block里访问成员变量。很多人以为成员变量不通过self访问就不会持有self这是误区。在block里直接写_name编译器会捕获self来访问这个ivar所以依然泄漏。第三段更隐蔽block被一个单例对象持有比如网络层管理器的回调数组。即使你对self用了weakSelfblock本身却被长期持有只要self不主动移除这个block还是会造成类似泄漏的效果。这个场景在面试中属于进阶考法但笔试里也出现过。提示答题时别只写结论。先说明“谁持有谁”的关系链再给修复代码最后补一句weakSelf修饰后要注意在block内部使用strongSelf防止执行过程中self被提前释放。这样答案完整度会高很多。2.3 weak指针的实现原理从SideTable到自动置nil追问weak实现原理是每套题的保留项目。如果只看一层就是“weak指针在对象释放后自动变成nil”但要拿到高分你需要把底层链路说完整以下均基于现代Objective-C运行时实现所有weak引用的地址都会登记在全局的SideTable里SideTable中有一个weak_table以对象地址为key存储了一个weak_entry_t。对象释放时dealloc会调用objc_destructInstance和clearDeallocating进入weak_clear_no_lock遍历对象的weak_entry_t把所有weak指针置为nil最后移除这个entry。这个过程发生在对象引用计数归零的那个瞬间使用了自旋锁现在已变为os_unfair_lock来保证线程安全。所以weak自动置nil不是一个“魔法”而是一套链表遍历加锁清理的过程。笔试里如果让你说weak和assign的区别你就把这条链路讲出来证明你真的读过源码而不是只背了结论自然就能比大多数考生多拿分。3. Runtime与底层原理题消息机制怎么答才不落俗套3.1 从objc_msgSend到动态方法解析的完整链路Runtime几乎是iOS笔试的压轴常客但大多数人的答案都停留在“消息发送、消息转发、Method Swizzling”这三个名词上缺少细节。我建议你按这条链路去答当调用一个OC方法编译器会把它转换成objc_msgSend(receiver, selector, ...)。在objc_msgSend里首先通过isa指针找到类对象然后在类的cache_t里查找方法缓存命中就直接调用IMP。缓存未命中就沿着继承体系逐级查找method_list找到后填充缓存并调用IMP。都找不到进入动态方法解析阶段调用resolveInstanceMethod:你有机会用class_addMethod动态添加方法。如果resolve没处理接着走消息转发先forwardingTargetForSelector:看能不能把消息转给别的对象再走methodSignatureForSelector:和forwardInvocation:做完整的消息转发。这套链路在笔试里经常包装成一个问题“向一个没有实现某个方法的对象发送消息会发生什么”很多人的回答是“崩溃”准确说应该是先经过动态方法解析和消息转发两次补救机会都没有处理时最终才触发doesNotRecognizeSelector:崩溃。如果你能把这个过程分阶段讲清楚再举一个实际使用动态方法解析的例子比如实现一个简单的懒加载路由用resolveInstanceMethod动态添加方法这道题基本就拿满了。3.2 Method Swizzling为什么容易翻车Method Swizzling也是高频题但很多人只知道用method_exchangeImplementations交换两个方法的实现不知道这里有三个关键坑第一Swizzling应该只在load里执行一次现在更推荐用dispatch_once嵌套保护防止父类子类重复交换。第二交换后的self不能用来调用原方法因为IMP已经交换了你调用的其实是对方正确做法是用class_getInstanceMethod拿到selector和IMP后手动调用原IMP。第三如果父类没有这个方法而你在子类里直接给父类添加方法会污染父类解决方法是先检查父类是否有这个方法没有就先补一个。我和团队曾经用Swizzling做全局页面埋点交换viewWillAppear:上线后部分页面出现时序异常后来排查出来就是同一个类被多次load触发重复交换。所以笔试题里如果问Swizzling的注意事项你把“线程安全”“重复交换”“父类污染”这三点写出来证明你不只是知道这个API是真的在项目里趟过坑比任何八股答案都打动人。3.3 关联对象与KVO实现原理关联对象AssociatedObject也是Runtime经典考点核心是它并不是给类添加实例变量而是通过一个全局的AssociationsHashMap以对象地址为key存储一组关联属性的值。对象释放时在dealloc中会调用objectivec_object中的_object_remove_assocations把关联对象释放掉。KVO的实现原理同样值得答全系统通过运行时动态创建一个子类NSKVONotifying_XXX把观察对象的isa指针指向这个子类重写被观察属性的setter方法在setter内部调用willChangeValueForKey:和didChangeValueForKey:再通过observeValueForKeyPath:ofObject:change:context:通知观察者。这个子类是在运行时生成的不是提前写好的类。笔试中如果把KVO和Runtime放一起考你可以顺带提一句KVO的触发条件依赖setter方法被调用直接修改成员变量不会触发这一点在手动触发KVO的场景里尤其重要。这种小细节在答题里很加分。4. 多线程与并发题货拉拉场景下的线程安全与请求调度4.1 GCD死锁为什么viewDidLoad里的同步任务会卡死GCD死锁几乎是必考题出题形式通常是这样主线程在viewDidLoad里调用dispatch_sync(dispatch_get_main_queue(), ^{ ... })问你程序会怎样。标准答案是死锁。原因是dispatch_sync会阻塞当前线程等待block在目标队列上执行而这个block被提交到主队列后要等当前主队列正在执行的任务结束后才会执行。可当前主线程正在执行viewDidLoad的代码如果viewDidLoad中调用了dispatch_sync主线程会等待block执行而主队列又在等主线程处理完viewDidLoad再派发block形成互相等待谁也无法继续。这里想补充一个大家容易忽略的衍生知识点如果你在任意一个串行队列里调用了dispatch_sync到自己同样会死锁。我在实际项目里就遇到过在自定义串行队列中执行任务时内部又用了同步派发到自己队列导致偶现卡死。根因就是这个串行队列一次只能执行一个任务队列里的任务在等待另一个自己队列的任务等于自己的左手指望右手先动。类似问题排查起来很恶心因为现场不可复现稍纵即逝。遇到这种场景最稳妥的做法是异步派发或直接调用。4.2 网络请求与地图轨迹上传并发量控制的工程化答案货拉拉的业务里司机端在地图页时既要上传GPS轨迹又要刷新订单状态还可能同时上传照片。如果网络层对所有请求不加控制所有请求一起发弱网下就会互相抢占带宽反而更快超时。这类问题的笔试考法往往是“如果一个页面上有多个网络请求同时发起如何统一管理”比较好的答题思路是这样分层用NSOperationQueue做请求队列设置maxConcurrentOperationCount比如限制同一时间最多并发3个上传请求。对请求做优先级管理GPS轨迹上报优先级高于普通日志低优先级请求会在高优先级任务多时被延迟执行。对网络层做一个统一的回调管理在控制器销毁时自动取消该页面所有未完成的请求避免回调到已释放对象。必要的时候引入依赖关系比如只有司机确认接单成功后才启动后续的行程上报任务。我自己的项目里就遇到过大量并发瞬间把客户端带宽打满的情况后来通过NSOperationQueue控制并发数并给请求增加优先级整个弱网环境下的成功率提升非常明显。这题如果在笔试里出现你就把并发控制、优先级、自动取消三层都写出来再补一句“GPS轨迹上传是高频但小包所以要单独的串行队列防止阻塞”会非常贴近业务阅卷人一看就知道你有真实考量。4.3 线程安全与锁atomic也不保证数据安全线程安全题基本都会提到atomic。很多人以为atomic是给属性加锁其实它的含义是“对属性的读写操作是原子的”也就是在返回或赋值getter/setter时加了自旋锁现代实现为os_unfair_lock保证你在多线程环境下get和set不会因为指令交叉而读到「半初始化」的值。但它不保证业务层面的数据一致性比如你有一个可变数组两个线程同时调用addObjectatomic只能保证数组属性的指针安全不能保证数组内部的线程安全。答题时你可以给出工程上更可靠的方案一是用串行队列做读写的统一入口读写都提交到同一个串行队列二是给关键代码加互斥锁比如NSLock或dispatch_semaphore三是尽量把可变状态收拢到一个模块避免全局变量满天飞。这三个方案在真实项目里都有明确使用场景写出来比只说“加锁”要具体得多。4.4 NSOperation的依赖关系把并发任务组织成流水线另一类多线程题会绕开GCD让你用NSOperation完成一组有依赖关系的任务。比如上传货物图片前必须先压缩、再加密、再上传。NSOperationQueue天然支持依赖B操作依赖A操作完成后再执行用addDependency:就能串联起来。笔试里如果让你比较GCD和NSOperation的区别我建议答出四个层次GCD更轻量、更底层适合简单的派发任务NSOperation可以设置依赖、取消操作、控制并发数更适合业务逻辑复杂的任务编排NSOperation是基于GCD之上的抽象所以优先使用NSOperation管理复杂业务任务。另外NSOperation的取消机制比GCD手动dispatch_source_cancel要方便很多这一点在长任务场景里特别重要。5. 网络层与数据持久化大文件传输、离线同步和缓存策略5.1 断点续传设计从HTTP Range到本地分片状态管理货拉拉这种业务里司机在车库、隧道里上传货物照片或证件图片弱网环境断连太普遍了。所以网传卷子里对“大文件断点续传的设计方案”这类应用题的考察大概率与真实业务强相关。断点续传的基础是HTTP的Range头客户端告诉服务器“我这次要读取文件的从第N个字节到第M个字节”服务器返回206 Partial Content就能只传缺失部分。但工程上难点不在协议而在客户端怎么管理已经传了哪部分把一个文件分割成固定大小的分片比如256KB每上传完一片就在本地记录片索引和校验值写入数据库。App重启后从数据库读取已上传的片索引跳过已完成的片从第一个失败的片开始续传。所有分片上传完调用一个merge接口通知服务端合并分片。每个分片要带唯一文件ID一般由服务端下发不能只靠文件名因为文件名可能重复。我见过最简单可靠的实现是每片上传成功后在本地一个plist文件或SQLite表里写入完成标记绝不依赖内存状态。因为App随时可能被杀掉内存里的状态说丢就丢断点续传的可靠性必须建立在落盘状态上。5.2 离线数据同步冲突时间戳合并还是增量拉取司机端网络不稳定订单数据在本地会产生未提交变更等网络恢复后要和服务端同步这就牵扯到离线数据一致性。笔试中的考法通常是“你怎么设计离线消息或离线订单的同步机制”。这里需要对两种策略比较清楚策略实现适用场景缺点全量拉取网络恢复后拉取服务端全量数据替换本地数据量小业务结构简单数据量大时浪费流量服务端压力大增量拉取本地记录最后同步时间戳拉取该时间之后的变更订单、轨迹、消息等频繁更新的业务需要服务端支持时间戳增量接口操作日志重放本地记录所有未提交操作恢复后按顺序重放离线修改较多需要保证操作顺序冲突处理复杂需要业务层去重合并策略服务端和本地都保留版本号/时间戳合并时按规则取舍多端同时编辑场景规则难定义容易丢数据工程上的稳妥方案是“增量拉取操作日志重放两者结合”数据同步走增量接口而本地未提交流水比如订单状态修改按序重放同时每条数据带updated_at版本号服务端合并时以版本号大的为准。离线同步类问题只要你能把时间戳、版本号、操作日志三个要素用起来答题就比只提“快照”完成度更高。5.3 缓存策略内存缓存、磁盘缓存与LRU淘汰网络层缓存策略也是高频考点。一套可用的缓存设计需要同时覆盖两层内存缓存层用NSCache做键值对存储已解码好的数据模型。NSCache在内存吃紧时可以自动清理部分条目比NSMutableDictionary更安全。磁盘缓存层把URL的MD5作为文件名原始响应数据写入沙盒Caches目录下次先查磁盘再回源。缓存淘汰策略通常基于LRU最近最少使用。LRU在iOS里实现可以直接用YYCache或在自己的双向链表上做实现。笔试中如果问LRU实现要点你就说“哈希表双向链表”get和put都是O(1)复杂度。然后补充图片缓存场景里还可以设置过期时间比如地图瓦片缓存7天订单列表缓存30分钟因为不同数据的热度差异很大。我自己的习惯是接口缓存统一做但设置“只在无网或弱网时读取缓存有网时优先回源”的策略避免用户看到一个永远不更新的列表。放在UITableView场景下这个策略能保证即使服务端挂了用户也能看到最近一次成功加载的数据。6. 架构与设计题组件化、MVVM与业务解耦6.1 为什么货拉拉这类O2O业务必须做组件化架构设计题在这套卷子里通常是一道开放性论述“请设计一个适合当前业务的组件化方案。”这类题没有唯一答案但阅卷人很容易看出你有没有真正经历过大型工程。货拉拉的iOS端有订单、地图、支付、IM、用户中心、营销等多个业务线如果全都写在一个target里编译时间会越来越长模块间的隐式依赖会越来越多。比如订单模块需要弹一个支付收银台直接import支付模块的类第二天支付模块改了个初始化方法订单模块编译就挂了。这还只是编译阶段运行时的耦合更麻烦。组件化的核心价值不只是“让代码好看”而是让团队可以按模块独立开发、独立测试、独立发布。这样订单组和三方支付组可以并行推进互不阻塞。笔试里回答组件化时一定要先讲清楚动机团队规模变大、业务边界变复杂、编译效率变低才能引出方案。6.2 组件化方案怎么选URLRouter、Target-Action还是Protocol目前iOS组件化的主流方案有三类笔试中选一种展开讲即可但最好三类都提一句证明你有全局认知URLRouter方案CTMediator就是这种方式通过performActionWithUrl把调用参数URL化路由中心做分发。优点是动态化程度高甚至可以由服务端下发跳转URL缺点是参数要包成字典编译期安全检查弱类型容易写错。Target-Action方案也是CTMediator早期的核心思路。用Target对象包装组件的对外方法用Action selector作为入口。优点是调用比较规范没有URL解析的额外开销缺点是Target-Action本质还是动态调用编译期无法校验是否存在。Protocol-Class方案组件对外只暴露协议通过一个注册表拿到实现了该协议的类实例。这是目前最接近类型安全的方案但需要维护注册关系和协议版本。如果要答题我的建议是先说明实际项目里可以把三种方式组合使用。远程页面跳转用URLRouter模块间方法调用用Target-Action做兜底核心业务服务用Protocol-Class保证类型安全。最后再补充一句组件化不是银弹如果团队只有5个人、业务单一强行组件化只会增加维护成本控制在两个target以内更实际。这种回答既有高度又接地气。6.3 MVC、MVVM在iOS侧的落地边界谈到架构MVVM也是必聊的。这里其实考察的是你对“视图、模型、视图模型”三者关系的实际理解。很多人的理解只停留在“VM就是给View准备数据的对象”但面试官想听的往往更细MVC里Controller越来越臃肿是因为它承担了太多不属于它的责任网络请求、数据解析、页面跳转、视图更新。MVVM是把这些逻辑搬到ViewModelView和ViewModel通过绑定机制RAC、Combine或自己封装的Block回调通信Controller只负责创建View和ViewModel并处理导航逻辑。但MVVM的坑在于绑定机制会让隐式调用变多代码追踪变难小页面用MVVM反而增加复杂度所以不应该教条化一个视图控制器堆了几千行再考虑用MVVM拆分也不迟。这道题在笔试里通常是论述题你按“为什么-怎么做-边界在哪”三层答基本就能覆盖到大多数人写不出的深度。7. 实战题地图、列表、大图场景下的性能优化细节7.1 地图页面性能3D touch、大量标注点与轨迹绘制优化货拉拉的司机端地图页往往同时显示几百个车辆位置或订单点。地图类的性能优化题在这套卷子里的出现概率很高。容易被忽略的坑和对应思路包括每个车辆标注如果是独立的UIView几百个View同时挂在地图上层GPU和CPU都会受不了。正确做法是使用地图SDK的聚合标注能力并按缩放级别决定显示哪些层级的标注。轨迹绘制的多段线如果每一段都建一个Polyline对象内存和绘制性能都差。把轨迹拆分不一定比合并好但最少要做抽稀删除直线中间的多余点再手动合并成最大的连续线段。地图上的气泡弹窗频繁创建销毁会产生大量内存碎片。用复用池管理气泡控件类似UICollectionView的cell复用是一个很加分的方案。如果笔试里问“地图页卡顿怎么排查”你还可以往图层混合方向想减少半透明图层、把气泡背景图预先生成、避免在drawRect里做耗时操作。这些细节看起来普通但确实是地图卡顿优化的真实路径。7.2 列表流畅度从Cell复用到异步绘制列表性能题是每个iOS笔试的固定节目。这里说的是真的可以写代码拿分的部分从FPS监控到具体优化手段。先做监控用CADisplayLink统计一秒内实际刷新帧率找到真正卡顿的页面。Cell高度一定要缓存不要让系统每次滚动都重新计算heightForRowAtIndexPath高度计算很贵。图片异步解码从网络或磁盘拿到图片数据后用CGContext或ImageIO在子线程完成解码避免在主线程因未解码图片导致卡顿。这也是SDWebImage早就在做的事情。圆角、阴影、渐变这类视觉效果能不用就不用或者用预先绘制好的切图替代避免离屏渲染。我自己的体会是把“离屏渲染”“异步绘制”“预排版”三件事做扎实列表流畅度就能提升一大截。笔试里只要你能把这几件事连起来讲而不是零散报菜名阅卷人就知道你做过多线程列表优化。7.3 启动时间优化pre-main到底在干什么启动优化是另一类必考实战题。main函数执行之前的pre-main阶段很多人没有概念实际上它耗时最大的原因有三个动态库加载系统动态库和App内嵌的Framework每一个都要经过mmap、解析、绑定符号。类与分类加载Objective-C把所有类、方法、协议注册到运行时分类越多这个阶段越慢。构造函数执行C的静态初始化器和__attribute__((constructor))函数都会在main之前执行。优化手段对应为精简动态库数量、合并可以静态链接的库检查load方法能不用尽量不用减少全局静态对象和C构造逻辑。还有一个特别容易忽略的点把启动时不需要的类放在Freestanding的动态库里用懒加载方式加载也能显著降低pre-main耗时。笔试里答启动优化建议按“先量化再优化”的思路来先说明用Instruments的App Launch或环境变量DYLD_PRINT_STATISTICS统计时间分布再针对耗时模块给出具体优化手段。这种“先定位再动手”的思维在任何性能优化题里都不会错。8. 写在最后一些答题思路的分享复盘完这套题我的一个直观感受是笔试不是看你“记住了多少”而是看你在一个具体问题面前能不能把它拆解成一条清晰的链路然后给出有依据的选择。比如断点续传不会只问你“怎么用Range”而是把分片、状态存储、弱网补偿都串起来消息机制不会只让你背源码而是让你讲清楚动态方法解析和消息转发在工程中各自的用途。如果你正在准备iOS秋招我的建议是不要只看面经总结把每道题背后的源码和业务场景都过一遍。哪怕一天只弄懂一个点比如“weak为什么能自动置nil”也比你背30题有深度的多。笔试真正拉开差距的不是那些“背了就会”的知识而是那些“你没踩过坑就编不出来”的细节。比如我给客户调优地图页时发现一个气泡控件反复创建释放会导致内存增长不断换成复用池之后整个地图页的内存占用下降了接近30%这类经验是任何八股文里都不会写的。这套卷子虽然是2018年的但它对iOS基础知识和工程化思维的考察方式到今天依然适用。愿你能从这套题里提炼出自己真正需要补的短板然后带着“做过项目”的底气走进考场而不是带着“背过答案”的侥幸。