2018货拉拉iOS笔试题复盘:底层原理与架构设计要点解析

📅 发布时间:2026/9/1 6:24:45
2018货拉拉iOS笔试题复盘:底层原理与架构设计要点解析
2018年秋招我投了货拉拉的iOS岗位。差不多过了简历筛选先做了一套线上笔试之后又收到一份笔试题就是标题里这套“卷三B”。当时看到卷三B这个编号还以为是多难的东西实际做下来发现它不像很多公司那样只考一堆概念默写而是把OC底层机制、日常开发中的坑、架构选型、甚至简单算法全部揉在一起既考基础也考项目里真正的反应速度。这套题做完之后我复盘了很久很多考点后来在其他公司面试里也反复出现。如果你是正在准备iOS面试的开发者不管是应届生还是转岗一两年想跳槽的这篇复盘应该能帮到你。我会按试卷的题型分布、高频考点、答题思路、以及我当时踩过的坑来拆解顺便把一些常见问题的标准答法整理出来。这样你至少知道这套题在考什么以及面试官出题背后想看到什么样的答案。1. 卷三B的整体结构与考点地图1.1 题型分布和时间分配我当时拿到的卷三B分成四块选择题、填空题、简答题和两道手写代码题。选择题大概十道填空题八道左右简答题四道最后是两道编程题。整套题的题量不算大但覆盖面很广从KVO、Block、RunLoop到网络层设计、数据持久化都有涉及。做完回头再看这个结构其实很像一次“iOS工程师能力体检”考察的不是某个孤立知识点而是你对整个iOS开发链路有没有完整认知。时间分配上我的建议是选择题控制在15分钟内填空题10分钟简答题和编程题留足40分钟以上。很多人在做选择题时反复纠结反而压缩了后面手写代码的时间这是最容易失分的地方。实际笔试中代码题往往占分比重最大哪怕前面某道填空不确定也不能影响编程题的完整实现。另外货拉拉的试卷是线上答题有些题目需要直接敲代码平时在本地Xcode里写得顺畅换到网页编辑器里会不太习惯建议提前用网页版代码编辑器练练手。1.2 核心考点的优先级判断从卷三B的实际题目来看核心考点可以分成三层第一层是语言基础比如属性修饰符、ARC、Block、代理和通知的区别第二层是运行时与线程模型比如Runtime消息发送、RunLoop运行模式、GCD死锁场景第三层是架构与工具链比如MVC和MVVM怎么选、网络层怎么封装、缓存策略怎么做。笔试题里还穿插了一些代码阅读题比如给你一段Block的代码问你输出什么这类题本质是在考内存管理和闭包捕获规则。我当时做完整套卷子的感受是它最看重的不是“你知道多少API”而是“你能不能解释清楚一个机制背后的原理”。举个典型的例子试卷里问“weak属性为什么在对象释放后会自动置为nil”如果你只回答“因为weak表会清除指针”大概率只能拿一半分。好的回答应该从Runtime全局弱引用表、对象释放时的清理流程、以及为什么用weak而不用unsafe_unretained这几个角度展开。后面我会更详细地拆解这类答题思路。2. 基础题解析OC语言与内存管理2.1 属性修饰符不只是背答案卷三B选择题和填空题里属性修饰符出现频率很高。比如同样一道题“ARC环境下声明一个NSString属性应该用strong还是copy”很多基础不牢的人直接选strong理由是我只要持有它就行了。但如果赋值的是一个NSMutableStringstrong只是增加了引用计数原对象后续内容一旦变化你的属性值也会跟着变这就可能引发数据错乱。用copy则会生成一份不可变副本后续修改原字符串不影响属性值这才是多数场景下的正确选择。再往下挖一层面试官很可能追问copy出来的对象是什么类型的这里有个很常见的坑[mutableString copy]得到的是不可变对象[mutableString mutableCopy]得到的是可变对象。所以你声明的是NSString用copy修饰后实际指向的对象可能是NSConstantString也可能是NSTaggedPointerString甚至在某些情况下是NSLocalizableString。如果代码里对这个对象做了isKindOfClass:判断以为是NSString结果却是某个私有子类就很容易出问题。答题时能举例说明这些细节比干巴巴背“copy是深拷贝”要有说服力得多。另外属性修饰符中atomic和nonatomic也是高频考点。很多人只知道atomic线程安全、nonatomic不安全但实际上atomic并不能保证对象操作的绝对线程安全它只保证getter/setter的原子性。举个例子一个atomic修饰的NSMutableArray属性两个线程同时进行addObject:操作仍然会出问题因为addObject是方法调用不是setter。这个细节点如果能在笔试里写出来面试官会觉得你是真的踩过坑而不是只会背书。2.2 ARC和MRC从原理到坑点卷三B的简答题里有一道关于ARC的问题大意是问ARC会不会产生循环引用为什么我当时写的是ARC只是编译器在合适位置自动插入retain/release/autorelease调用它不会帮你在业务层面解决“对象互相持有”的问题。只要对象之间存在强引用环就依然会造成内存泄漏。常见的循环引用场景包括Block捕获self、代理属性用strong、NSTimer强引用target等。关于NSTimer的循环引用这里有个很实际的坑很多项目里把timer声明为当前控制器属性然后[NSTimer scheduledTimerWithTimeInterval:target:self selector:...]。这种写法下runLoop强持有timertimer强持有self如果这个self又强持有timer整个环就形成了。解决办法是iOS 10以后的block版本timer配合weakSelf或者使用__weak代理类做中间层。笔试作答时不能只写“用weakSelf”最好能说清楚block为什么还要用__weak以及__strong修饰在block内部的必要性。ARC在代码阅读题里的表现更明显。试卷里给了一段典型的Block代码__block NSInteger count 0; void (^block)(void) ^{ count; }; block(); NSLog(%ld, (long)count);如果你不了解__block的作用可能会把输出结果写错。__block本质上是让外部变量被捕获到堆上block内部修改的是这个堆上的变量外部读取时也是同一个地址所以输出是1。这类题看起来简单但很多人会因为对“捕获变量”理解不透彻而答错。我建议在准备笔试时亲手把几种变量捕获情况打印一遍普通局部变量、static变量、全局变量、__block变量看看到底谁能改、谁能引用理解了就不怕变着花样考。2.3 内存管理实战autoreleasepool在什么时候有用还有一道填空题是考autoreleasepool的。题目说“在for循环中创建大量临时对象为什么需要加autoreleasepool”如果你只是说“为了释放内存”不够精确。更好的回答是在没有主动加入autoreleasepool的情况下临时对象会被当前线程的runLoop产生的autoreleasepool延迟释放如果循环次数很多峰值内存会一直涨。加上autoreleasepool后每次循环结束都会立即清空这一次创建的临时对象。实际项目中类似场景出现在批量处理图片缩略图、读取大文件生成多个对象、或者加载大量网络数据转模型时。我当时在笔试题里举了个例子用SDWebImage下载1000张图每张都转成UIImage如果不处理峰值内存很容易收到内存警告。但注意也不要为了用而用小规模循环加autoreleasepool反而会带来不必要的性能损耗因为入池出池本身有开销。这个“度”的把握面试官往往会追问能答出来就是加分项。3. 运行时与线程模型RunLoop、GCD与多线程安全3.1 Runtime消息传递与常见应用卷三B简答题部分有一道特别经典的Runtime题“OC中调用一个方法底层发生了什么”完整的流程是先通过对象的isa指针找到类对象然后在class的method list里查找对应SEL的IMP如果找不到会沿着superclass继续查找依然找不到就会进入消息转发流程。消息转发分成三步动态方法解析、快速转发、完整转发。每一步都有对应的方法可以重写比如resolveInstanceMethod:、forwardingTargetForSelector:、methodSignatureForSelector:和forwardInvocation:。这个问题的价值在于它不只是考原理而是考你能不能拿它来解决实际问题。我当时在答案里补了个例子项目里通过Method Swizzling统一处理按钮点击事件上报实现了不侵入业务代码的日志统计。不过要注意Swizzling的写法很有讲究一定要在load方法里用dispatch_once保证只执行一次交换方法时最好用class_getInstanceMethod获取原始Method避免子类父类交换错乱。这些细节写上去会让面试官觉得你是真的在项目里做过而不是单纯背了Runtime概念。还有一道填空题问KVO是怎么实现的。KVO本质也是Runtime动态创建了一个中间类NSKVONotifying_xxx重写被观察属性的setter方法。很多开发者只知道“添加观察者”但不知道系统会改变对象的isa指针导致用object_getClassName打印出来的类名不是原来的类。这类隐藏细节在笔试里很受欢迎因为能区分出有实战经验和只看文档的人。3.2 RunLoop的运行模式与卡顿优化RunLoop是iOS面试里绕不开的考点卷三B也考了。问题大概是RunLoop有哪几种运行模式我们平时主线程的RunLoop是做什么的常规答案是DefaultMode、UITrackingMode、CommonModes加上Connection和Initialization等模式。但面试官更想听的是为什么滑动TableView时图片延迟加载要放到DefaultMode而不是CommonModes因为当用户拖动ScrollView时RunLoop会切换到UITrackingModeDefaultMode下的任务被暂停图片加载就不会抢占UI流畅度。从这套题出发我建议你顺便把卡顿优化的思路捋一遍。卡顿的本质是主线程在某一轮RunLoop里做了太多耗时操作导致没有及时处理下一个事件。检测卡顿可以在子线程里监听主线程RunLoop的状态变化如果kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间耗时过长就记录当前主线程的堆栈。这个思路在工具型和Bugly等监控平台里都有应用。笔试时未必要求手写但如果你能在答案里提到“利用RunLoop状态监控卡顿”并且说清楚监听哪个状态会比泛泛而谈“不要在主线程做耗时操作”更有深度。3.3 GCD死锁场景与多线程安全方案GCD题目在卷三B里有两道一道是选择题问以下代码会不会死锁dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(hello); });答案是必然死锁。因为主线程执行这段代码时同步派发阻塞了主线程而任务又需要主线程执行形成互相等待。类似地在一个串行队列里执行sync到同一个队列也会死锁这与队列是串行还是并发无关关键是“当前执行者”和“目标队列”是否同一条串行队列。这个点如果不理解很容易只记住“主线程不能用sync”而不知道原理。多线程安全方面卷子里问到了“怎么保证数组的线程安全”。我当时的答案分了几层最简单的是加锁但要分清楚用synchronized、NSLock还是os_unfair_lock更好的做法是使用并发队列加栅栏读操作并发写操作用dispatch_barrier_async这样读多写少的场景性能更好再进一步如果你的数据结构比较简单也可以用读写锁或者直接用系统提供的线程安全容器但iOS里没有绝对的线程安全数组很多时候还是要封装。笔试时最好把方案优缺点都列出来不要只写“用锁”。4. 架构题设计模式与iOS应用分层4.1 MVC、MVVM还是MVP卷三B简答题最后一道我记得是问如果货拉拉的App要重构首页列表模块你会怎么设计架构这个题目看起来开放实际是考你对常用设计模式的理解。我需要先明确一点没有万能架构只有合适当前业务和团队规模的架构。经典MVC在iOS里的问题是Controller太重所有逻辑都堆在Controller里导致后续测试困难、维护成本高。MVVM引入ViewModel做数据转换和业务逻辑配合双向绑定能让Controller瘦身但学习成本和回调链复杂度也会上升。我当时的回答是优先考虑分层架构网络请求和模型解析放在数据层页面状态管理放在ViewModel层视图层保持被动渲染。如果业务简单比如只有一个详情页用MVC就够了强行上MVVM反而多一层间接跳转。如果后续有大量表单校验、数据聚合MVVM更合适。另外还可以补充一句无论MVC还是MVVM关键是把“不变的部分”和“变的部分”分开比如网络层、缓存层、路由层做到只依赖协议不依赖具体页面这样后期替换成本低。4.2 网络层封装与数据解析这套笔试题里还有一道关于网络层的简答题怎么设计一个可扩展的网络层这属于日常开发里最常碰到的设计问题。我当时从几个点展开统一请求入口、统一错误处理、统一参数签名、统一日志和埋点。第一层是网络工具可以用NSURLSession封装不直接依赖AFNetworking方便后续替换第二层是业务请求层把每个接口封装成对应的方法方法内部处理公共参数、签名、请求头、超时时间第三层是回调层把泛型解析和数据转换放进来对外只提供成功回调和失败回调。关于数据解析货拉拉的题目里很可能给出了一个JSON结构让你写model解析逻辑。这里有个小技巧不要直接在ViewController里做JSON转Model而是把解析放到Model的分类或者专门的Mapper类里。我用的是YYModel因为它支持自动类型转换比如字符串转数字、下划线转驼峰命名。如果你手写initWithDictionary:一定要注意健壮性不能因为某个网络字段缺失导致整个解析崩溃。笔试题里常犯一个错拿到字典后用dict[key]然后直接赋予属性没有处理为空的情况。这种小细节面试官一眼就能看出你有没有联调经验。4.3 数据持久化与应用缓存策略和网络层配套出现的是缓存题。卷三B里考了一道App的首页数据缓存怎么做关注点有两点一是数据模型需要实现NSCoding或使用其他序列化方式二是缓存策略要考虑联网失败、断网、以及数据过期机制。我见过很多初级解法是直接把字典数组写进UserDefaults或者用NSKeyedArchiver归档模型。这两种方式在数据量小的时候没问题但一旦字段变动比如新增一个属性旧缓存读取时就会因为decodeObject返回nil而出错。更好的方案是使用SQLite或FMDB存结构化数据或者用WCDB做数据层性能更强且支持增量更新。缓存策略上我习惯把“缓存有效期”和“网络状态”结合起来先读缓存显示再请求最新数据刷新如果本次请求成功更新缓存并写入时间戳如果请求失败且当前有缓存就展示缓存并提示“网络不可用已显示上次数据”。这个思路在写架构答案时很加分因为它体现了你对真实用户场景的思考而不是简单停留在“存进去、读出来”。5. 手写代码题算法与常用实现5.1 字符串与数组的经典手写题卷三B的编程题第一道我记得是字符串反转。题目可能不是让你直接用reversedString而是要求写出具体实现考察基本功。答案可以很简单- (NSString *)reverseString:(NSString *)string { NSMutableString *result [NSMutableString string]; for (NSInteger i string.length - 1; i 0; i--) { [result appendString:[string substringWithRange:NSMakeRange(i, 1)]]; } return result; }但要注意这种写法实际项目中性能很差因为substringWithRange:每次都会创建新字符串。更好的做法是先把字符串转换成C字符数组或NSArray再首尾交换。另外题目如果扩展为“反转字符串中的单词顺序”比如hello world变成world hello除了用split和逆序遍历还可以用整体反转后局部反转的做法空间复杂度O(1)。准备这类题时不要只背一种解法要想想时间复杂度和空间复杂度怎么被优化。数组和字符串的题目通常还会考到“找两个数组的交集”“去除数组中重复元素”“判断字符串是否回文”等。货拉拉作为同城货运平台业务上可能会和“路线”“距离”“配送”相关虽说不一定会直接考算法题但常见的排序、查找、去重技巧还是应该熟练掌握。我在实际笔试里遇到一道“找出数组中出现次数超过一半的数字”当时先想到字典计数然后才想到摩尔投票法。如果你能在答卷里写出摩尔投票法并且解释为什么它能做到O(n)时间和O(1)空间会明显比普通计数方案出彩。5.2 链表与二叉树的必刷题型另一道编程题考了链表反转。很多应届生在这里翻车因为平时工作主要做UI和业务逻辑链表已经很久没碰了。反转链表的核心是三个指针pre、current、next。循环里先保存next然后让current.next指向pre再整体后移最后返回pre。这个代码量不大但一定要能在纸上写对。我建议准备笔试时把链表的增删改查、反转、合并两个有序链表的代码都在Xcode里跑一遍不要光看思路。二叉树的部分卷三B未必考了但考虑到“卷三B”这种编号很可能还有A卷或其他套题二叉树的题也会出现在同类笔试里。重点掌握前序、中序、后序遍历的递归和非递归实现以及层序遍历、最大深度、最小深度、判断是否平衡二叉树。非递归遍历是个大坑很多人只会递归面试官一问“如果递归栈溢出怎么办”就懵了。如果你能写出用一个辅助栈实现前序遍历或中序遍历并解释为什么这样写会很有竞争力。5.3 代码规范与边界条件的细节手写代码题不只看最终答案还会看代码的健壮性。我当时就在反链表的题里漏了处理空链表和单节点的情况虽然思路对但边界没写好还是会扣分。写代码时要默认所有人都可能用超边界的数据来测试字符串可能为空数组可能有大量重复链表可能成环虽然一般不会让你处理成环但需要判断。在答卷里写出“如果node为nil直接返回nil”这样的防护代码能体现工程素养。另外一定要记得写注释。笔试环境不是IDE没有断点调试注释能让阅卷人快速理解你的思路。我习惯在代码块顶部先用自然语言写清楚解题思路再写代码这样即使代码有细节遗漏阅卷人也能看到思考过程。货拉拉的笔试题偏实用它不一定要求你写出最优解但一定会看你是否具备可维护、可读写的代码表达能力。6. 复盘总结与后续面试建议6.1 卷三B里最容易失分的三个地方第一选择题中关于属性修饰符和Block捕获的细节很多人凭感觉选没有真正理解底层。第二简答题中“描述一个你熟悉的网络请求流程”这类题回答得过于概念化没有结合项目实际。第三手写代码题的边界条件漏写以及代码风格混乱缺少注释。这些都是可以在考前通过针对性练习避免的。我特别提醒一个点不要只刷“面试题集合”而不去写Demo。像RunLoop、Runtime、Block底层这些内容光看答案记不住最好自己写几个测试工程把对象打印出来观察retain count虽然是debug把RunLoop状态打日志体验一下消息转发流程。只有亲手操作过在笔试和面试时才能说得有底气。6.2 从笔试到面试如何延续这份优势如果笔试过了后面通常是技术面。货拉拉的面试官大概率会围绕你的笔试题答案继续追问。比如你写了“用MVVM”他就可能问你ViewModel和View之间的关系怎么处理kebab-case和camelCase的映射怎么做数据绑定用的是KVO还是Block还是RxSwift。再比如你写了“用os_unfair_lock做线程安全”他就可能问你为什么不用NSLock以及os_unfair_lock在什么情况下会死锁。所以笔试时的每句话都要做好被追问的准备。我在笔试时习惯把答案写得比题目要求的更细一层题目问“怎么保证字典线程安全”我就顺便写了操作封装和性能取舍题目问“怎么优化启动速度”我就写了pre-main阶段做了什么动态库加载、类注册、load方法耗时怎么统计。这些小延伸不会让阅卷人反感反倒能展示你的思考深度。前提是你要真的理解你写的内容不要为了显得专业而堆砌名词。6.3 给备考同学的一个实用练习清单对照卷三B的考点我整理了一个练习清单你可以按顺序过一遍属性修饰符对同一对象分别用strong和copy赋值对比地址和可变性。Block用__block、__weak、__strong改造同一个捕获场景打印结果。RunLoop监听主线程状态在滑动和空闲时打印状态变化。GCD用dispatch_sync到主线程、串行队列、并发队列观察是否死锁。Runtime手写一个Method Swizzling并在load方法里调用一次。网络层自己用NSURLSession封装一个请求工具支持GET/POST、超时、取消、回调。缓存用FMDB建表存模型并处理字段升级。算法反转链表、合并有序数组、字符串单词反转、两数之和。这套清单覆盖了iOS开发日常高频知识点。笔试题目千变万化但核心不变。你不需要把每个方案背到一字不差而是要学会解释为什么选它能说出使用场景和局限。最后再说点个人体会。货拉拉2018秋招这套卷三B虽然已经是几年前的内容但里面考的iOS底层原理和架构思路放到现在依然不过时。现在面试中有些公司更愿意考Swift和SwiftUI但OC内存管理、RunLoop、GCD仍然是存量项目的基础也是判断一个iOS开发者功底扎实与否的重要标准。我后来自己也参与过几次社招面试发现很多候选人在基础题上失分的原因不是不会而是缺少体系化梳理知识点散落在经验里没有形成一张完整的知识网。如果你正在准备iOS面试不妨以这套笔试题为镜子先把语言基础、内存管理、线程模型、架构设计这几块串起来再去补算法和实战细节效果会比零散刷题好很多。