
前几天整理旧硬盘翻出一份摩拜2018校招客户端开发iOS的笔试卷。那会儿正好是共享单车打得最凶的阶段摩拜作为头部玩家客户端团队对校招生的筛选逻辑很明确——不要求你有共享单车领域经验但iOS基本功必须扎实同时对完整业务链路的理解要有感觉。现在回头看这份卷子里面既有一些已经明显过时的技术细节也有一批直到今天面试还在翻来覆去问的硬核考点。这篇文章就把这份卷子从头到尾拆一遍结合我自己的复盘和平时带新人时看到的真实情况说一说每一类题目到底想考察什么、怎么答才能拿分、哪些地方最容易翻车。如果你正在准备移动端校招或社招或者单纯想看看2018年共享单车大战背后的技术体系长什么样这篇都值得花点时间读一读。1. 回溯场景摩拜客户端笔试卷背后的技术画像1.1 从业务推导出来的考察范围看一份笔试卷不能只盯着题目本身要先想清楚出题人站在什么立场。摩拜2018年的客户端团队面对的是一个日订单量千万级的业务核心链路是扫码开锁、地图找车、行程计费、支付结算。这些业务对iOS端提出的要求直接决定了笔试卷的出题方向。扫码开锁要求摄像头调用、图像识别、网络请求的链路完整可靠地图找车要求地图SDK集成、大量标注点渲染、定位与权限管理行程计费要求前后台状态切换、精准的计时逻辑、异常恢复机制支付结算要求网络层安全、订单状态一致性、弱网容错所以这份卷子虽然看起来是通用iOS题但每道题背后都挂着真实的业务场景。比如考多线程本质上问的是地图滑动时大量图片异步加载不卡顿的问题考网络层映射的是扫码后高并发开锁请求的稳定性问题。理解了这层对应关系你答题的时候就能往业务落地说而不是空对空地背概念。1.2 五个能力维度的考察逻辑我把这份卷子整体过了一遍发现它其实在筛选五个维度语言基础、系统机制、并发处理、架构意识、工程素养。语言基础考的是OC的Runtime、内存管理、KVC/KVO这些底层认知系统机制考的是RunLoop、生命周期、消息传递这些iOS独有的设计并发处理考的是GCD、NSOperation、线程安全架构意识考的是MVC的缺陷和MVVM的改进方向工程素养则通过场景题和开放题来试探比如如何设计一个扫码开锁的完整流程。这五个维度不是孤立的。语言基础和系统机制是底线答不好直接淘汰并发处理和架构意识决定你能不能干中级以上的活工程素养决定你值不值得培养。校招生没有太多项目经验所以出题人特别看重你在笔试里展现出的思维方式和问题拆解能力。2. 基础题拆解iOS必考四件套的答题逻辑2.1 内存管理的三道经典题摩拜这份卷子的基础部分第一题通常是循环引用。题目会给一段代码让你找出内存泄漏点并说明原因。这种题说白了就是考三个最常见的坑block、delegate、NSTimer。block的循环引用本质是对象持有blockblock又持有对象。比如一个ViewController持有dispatch_after的blockblock里又用了self如果这个block在self释放后还执行就会造成self无法释放。解法是用__weak typeof(self) weakSelf self在block里再用weakSelf。但要注意如果是延迟执行的block光转weak还不够因为block本身被GCD的全局队列持有self虽然可以释放但block还是会执行weakSelf会变成nil逻辑可能出错。NSTimer是最阴的。你以为是Timer强持有了target导致循环引用实际上RunLoop强持有了TimerTimer强持有了target而你在ViewController里又强持有了Timer。打破这个环需要在viewWillDisappear或dealloc里invalid掉Timer同时注意Timer的target改成weak代理或者用block方式避免持有。这道题我见过太多人只答出用__weak修饰就完事完全没提Timer的invalidate时机这就是丢分点。还有一种常见问法weak和assign有什么区别很多人知道weak会自动置nilassign不会但不知道为什么。weak是iOS 4引入的它基于Runtime维护的weak表当对象释放时会遍历表把指向它的weak指针全部置nil。assign是纯指针赋值不参与对象生命周期管理所以如果对象被释放后你用assign指针访问就是野指针崩溃。答这道题要把底层机制讲透weak表的结构、对象释放时的清理流程、线程安全问题。2.2 autoreleasepool的实战意义这道题在卷子里出现的形式是解释下面代码中autoreleasepool起到什么作用给一段for循环里创建大量临时对象的代码。如果是MRC时代autoreleasepool是内存管理的必修课。ARC时代很多人觉得它没用了其实不是。系统在很多地方隐式使用autoreleasepool比如RunLoop每次事件循环的autoreleasepool drain、autoreleasepool块。如果你在for循环里创建了大量临时字符串、UIImage对象这些对象会被加到当前autoreleasepool里直到池子被释放才真正释放。如果池子很大内存峰值就会很高。实测一下一个循环创建100万个NSString对象不包autoreleasepool的话内存峰值可能到30MB以上包一层之后能降到个位数MB。在摩拜这种地图App里滚动时频繁创建标注View、图片对象如果不注意这个内存就会忽高忽低严重时被系统杀掉。答题的时候一定要说清楚两个点一是autoreleasepool的作用机制二是在什么场景下需要手动加。2.3 多线程与并发不只是背API摩拜的卷子里多线程考得很重。有一道题是以下代码会死锁吗代码是dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(hello); });这题看起来简单但很多人栽了。在主线程执行这行代码时dispatch_sync是同步提交block到主队列主队列是串行队列当前正在执行的任务就是这段代码还没结束新的任务永远等不到执行机会形成死锁。所以答案是会死锁。引申问法还有如果在全局并发队列里执行这段代码会怎样答案是也会死锁因为dispatch_sync调度到主队列而执行这行代码的线程不在主队列里主队列如果空闲会执行但如果主队列正忙就等。另一道高频题是atomic是绝对线程安全的吗答案是否定的。atomic只保证属性的getter/setter原子性也就是读写安全不保证对象内部状态的线程安全。比如一个atomic的NSMutableArray两个线程同时addObject还是会出问题。摩拜这种业务里车辆位置数据是多线程写入、主线程读取的如果只依赖atomic修饰属性小概率会出现数据不一致。正确做法是使用串行同步队列做读写隔离或者用os_unfair_lock自旋锁保证临界区互斥。还有一道关于GCD和NSOperation区别的题标准答法有四个维度是否支持取消、是否支持依赖、是否支持最大并发数控制、是否支持优先级设置。但我想补充一点实际经验NSOperationQueue底层是基于GCD实现的但它提供了更高层的能力。在摩拜的地图模块里加载某个区域的车辆数据需要先请求基础地图数据、再根据视野范围请求车辆列表、最后按车辆类型过滤展示这三步有依赖关系用NSOperation的addDependency更直观比GCD的dispatch_group加信号量方案好维护。3. 场景题实战扫码开锁与地图渲染两道硬菜3.1 扫码开锁的业务链路设计摩拜的卷子一定有一道场景设计题最常见的是请设计扫码开锁的完整流程包括客户端、服务端交互以及异常处理。这题没有标准答案但考察的是你有没有完整思考过一条业务链路的常识。我的建议是把链路分成五个阶段扫码识别、车辆信息校验、开锁请求、开锁结果确认、状态同步。扫码识别阶段要用AVFoundation的元数据输出识别二维码内容是一串车辆ID的编码注意处理扫描框外误识别、模糊抖动导致的重复扫描要加防抖逻辑。车辆信息校验阶段要请求服务端确认车辆是否可骑、是否在服务区、是否有故障标记这里要考虑车辆状态缓存避免每次都全量请求。开锁请求阶段是重点。网络层要用短连接发POST请求带上车辆ID、用户token、当前GPS坐标、时间戳。服务端返回开锁指令后客户端通过蓝牙或蜂窝网络把指令下发给车锁。这里有个关键设计开锁指令的时效性。如果用户扫码后与服务端的通信延迟太高指令过期车锁会拒绝执行。所以请求要加超时时间摩拜当时定的策略是超时5秒自动取消并提示用户重试。异常处理是这题拿高分的关键。要主动列出场景用户扫码后手机没网、蓝牙断开、车锁电池耗尽、车辆被预约等。每个场景都要有对应的提示文案和兜底逻辑。比如蓝牙断开App要能自动重连并续传指令车锁没电服务端应该在信息校验阶段就返回不可用状态客户端显示车辆电量不足而不是让用户白扫一次。我当时带的一个实习生只写了正常流程异常处理一句带过这种答案在阅卷人眼里就是普通水平。3.2 大量车辆标注的渲染优化地图题也是摩拜笔试卷的常客。题目一般是地图上有数万辆单车如何保证渲染不卡顿。这题我从两个层面答数据层和渲染层。数据层先说懒加载车辆位置不能一次性全量拉取而是按当前可视区域bounding box分块请求。用户拖动地图时实时监听regionDidChange对新增区域发请求旧的车辆数据先缓存但临时隐藏。这需要一个数据管理模块维护当前视野内的车辆列表和对应的annotations。渲染层再说复用机制。这个和UITableView的cell复用原理一样MKMapView的viewForAnnotation返回的MKAnnotationView要dequeueReusableAnnotationViewWithIdentifier复用。因为地图上的annotation持有的是强引用如果每个pin都创建一个新View而不复用内存会线性增长。实测下来一个区域5000个pin不复用的话加载时间多两倍以上滑动时有明显卡顿。还要提聚合方案。当缩放级别低时同一个区域有几十辆车没必要全部展示可以把它们聚合成一个标注显示数量。可以自己写聚合算法比如按网格分桶在同一网格内取中心点作为聚合坐标显示数字放大地图时再逐渐拆分。也可以用第三方框架如BaiduMap的聚合库或高德的GestureBasedMapView配合自定义聚合逻辑。摩拜当时的业务量级是单个城市车辆峰值在十万以上视野内同时可见的车辆也可能过万聚合是刚需。4. 架构设计题从MVC到组件化的演进思路4.1 为什么共享单车App需要组件化卷子的架构题有一道开放题假设摩拜App有地图、扫码、行程、运营活动等多个业务模块如何组织代码结构这题考察的其实是组件化思想。单体App的问题很明显多个团队并行开发一个工程代码冲突率高、编译时间越来越长、模块间耦合严重。比如运营活动和扫码流程都依赖用户登录态如果登录态模块被某个团队改坏了其他所有模块都要跟着返工。组件化的目标就是把这些模块拆成独立的pod仓库每个模块有自己的版本号业务间通过路由通信。答这道题要给出完整的方案我一般分三层说基础层、中间层、业务层。基础层是网络库、存储库、埋点库这些纯技术组件不依赖任何业务中间层是登录、定位、支付等能力组件只暴露接口不暴露实现细节业务层是地图、扫码、行程等业务组件只依赖中间层和基础层的接口。这样可以保证依赖方向是单向的不会被逆向依赖。再往下要讲路由。组件间通信不能直接import头文件否则就失去意义。可以通过URL路由或Target-Action模式。URL路由的优点是动态性强可以远程下发跳转规则缺点是参数是字符串类型编译期无法检查。Target-Action模式是阿里的思路通过Runtime动态调用可以减少字符串匹配的复杂度但需要在每个组件里维护一个Target类。我当时在实际项目里是两种结合内部业务跳转用Target-Action外部运营活动落地页用URL这个方案在组件化改造的过渡期很实用。组件化还要考虑二进制化。组件多了以后全源码编译很慢可以把稳定的组件打成二进制包只在需要调试时才挂源码。摩拜当时的工程在组件化之前全量编译要10分钟以上组件化加二进制化之后缩短到3分钟以内这个效率提升对发版节奏非常重要。4.2 MVVM在客户端落地的取舍架构题还喜欢问MVC和MVVM的区别以及你在实际项目中怎么选。2018年正好是MVVM搭配ReactiveCocoa在iOS圈最火的时候摩拜这种规模的项目也在逐步尝试。MVC在iOS的问题在于ViewController承担太多职责。一个页面往往同时负责网络请求、数据加工、视图刷新、用户交互响应代码量轻松过千行。MVVM的思路是把数据加工的逻辑抽到ViewModel里ViewController只做绑定和管理生命周期。ViewModel的核心是输入-输出映射。输入是用户事件和外部数据输出是视图需要的展示数据。举个例子地图页的ViewModel接收用户位置、筛选条件、时间范围输出的是聚合后的车辆标注模型数组直接给ViewController渲染。这样ViewModel可以脱离UI做单元测试比如写一个测试用例验证用户选择距离最近时返回的车辆列表按距离排序是否正确。但我必须说MVVM不是银弹。ReactiveCocoa的学习成本高信号嵌套起来调试非常痛苦。如果想控制复杂度可以用更轻的方案ViewModel只做数据转换用block或delegate通知ViewController刷新不引入响应式框架用单向数据流的思想约束逻辑。我在实际项目中建议过这套轻量MVVM团队上手快收益也明显不会为了架构而架构。5. 踩坑复盘笔试现场最容易丢分的三个环节5.1 审题不清导致的答非所问这个现象在校招笔试里太常见了。比如题目问如何在iOS中实现单例很多人直接开始默写dispatch_once的代码然后讲线程安全这当然没错但题目如果带着业务背景要求保证在使用过程中不释放且线程安全那除了dispatch_once还要考虑到App被杀后单例状态丢失、主动重置的场景。摩拜的卷子里单例题的实际语境是定位管理器需要全局唯一但同时要支持切换定位精度模式那么仅仅写一个标准单例是不够的要设计setter方法来改变精度还要考虑多线程同时修改的问题。我的建议是下笔前先把题目读两遍标出所有限定词把基础版答案和加场景版答案分别列出来。基础版保证得分场景版是加分项。宁可多答一个相关知识点也不要只答一个方向的答案。5.2 手写代码的基础错误笔试基本都要手写代码很多人在IDE里写惯了一上手写就露怯。常见错误有三类类名方法名拼错、方法签名不完整、逻辑边界没处理。比如写单例时copyWithZone要返回selfrelease要直接return虽然是MRC时代的写法但手写经典题时会要求写完整写tableView的cellForRowAtIndexPath时很多人忘了重用标识符要注册或者忘了判空。我建议备考时用纸笔练几道高频题单例、block内存管理、KVO实现、深拷贝与浅拷贝、两个数组去重。这些题手写熟练度直接决定了笔试速度。平时写代码能自动补全的符号笔试时就可能卡壳。另外注意代码格式缩进、括号对齐这些细节。阅卷人一天看几百份卷子一份排版混乱的代码即便逻辑对也可能因为看不清楚被误判。这很现实。5.3 只回答是什么不回答为什么阅卷最无语的情况是答案背得一字不差但你追问一个为什么就答不上来。比如考RunLoop很多人能说出五种模式、common modes、addPort但问到mainRunLoop在什么时机被唤醒就懵了。RunLoop的机制要落实到源码层面去理解。它本质上是一个do-while循环每次循环处理source0、source1、timer、observer没事件时进入sleep有事件时被唤醒。为什么滑动TableView时NSTimer不走了因为在TrackingRunLoopMode下默认的Timer只被加入了DefaultRunLoopMode滑动时RunLoop切换了模式Timer就不被处理。解决办法是把Timer添加到common modes。这个问题摩拜的卷子确实考过我印象很深因为答出来的人不到三分之一。所以复习的时候每道题都多问自己三层它怎么实现的、为什么这么设计、如果换个场景会有什么问题。6. 这份卷子对今天的参考价值有人会觉得2018年的笔试卷已经过时了Swift都迭代到6.0了iOS 17、18都出了多少新特性还看这些干什么。但我个人觉得底层核心知识点远没有过时内存管理换到Swift里的ARC依然重要只是新增了值类型和写时复制并发从GCD到async/await但锁、竞态、死锁的概念依然一模一样网络题的TCP三次握手、HTTPS握手流程依然还是今天的面试热点。真正过时的是一些具体API和第三方库比如当年的ReactiveCocoa现在已经很少有人用了但这不影响你对MVVM理解框架的认知。我在带新人的时候还会拿这套题来给他们摸底。原因很简单它考察的不是某个版本的系统API而是作为iOS开发者最底层的思维范式。你掌握了这些思维新框架新API上手不过是一两天的事如果只是背了新API而底层逻辑不通项目一复杂还是会出问题。最后再分享一个小技巧现在准备笔试面试与其漫无目的地刷LeetCode和面试题不如把一套完整的旧真题翻出来按我上面说的思路认真复盘一遍把每道题的业务背景、考察点、答案逻辑讲给另一个人听。如果你能把自己讲明白那面试基本就稳了。这套方法我用过很多次带过的几个实习生靠这个思路拿到了不错的offer你也可以试试。