尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

摩拜校招iOS笔试题复盘:内存管理、多线程与弱网方案

摩拜校招iOS笔试题复盘:内存管理、多线程与弱网方案 今晚整理旧电脑翻出一份摩拜2018校招客户端开发iOS笔试卷。看着上面的笔迹当年备考查资料的场景一下被勾了出来。这份卷子在当时流传并不算广但含金量很高——它跳过了“某某API怎么用”这类背一遍就会的题目而是把大量篇幅放在Objective-C内存管理、多线程、网络请求以及界面性能上。之所以这样出题和摩拜客户端要面对的真实场景直接相关扫码开锁必须稳骑行轨迹必须准地铁口弱网环境下还得能正常支付、结束行程。说白了摩拜要找的不是“会用SDK拼页面”的开发而是能回答“开锁请求发出去之后App内部到底发生了什么”的工程型iOS开发。这份卷子考的内容放在今天去看也一点不过时。我在后面几年面试别人、带新人、做技术复盘时经常把里面的题拿出来当参考。所以我决定把整份卷子的考察逻辑、每道高频题背后的原理、以及我当时踩过的坑完整拆开来讲。如果你正在准备iOS客户端开发相关岗位尤其是想进共享出行、本地生活这类有强位置属性业务的团队这份拆解值得认真看。1. 从卷面看摩拜iOS团队想筛掉什么样的人1.1 一张卷子背后的业务场景即扫即走、弱网、续航很多人在准备笔试题时会犯一个错误只看题目本身不去想公司为什么出这道题。这样复习的效率很低因为你不知道面试官到底在找什么。摩拜这份卷子最大的特点就是几乎每道题都能从它的业务场景里找到出处。共享单车的使用流程非常短用户打开App、扫码、开锁、骑行、锁车、支付。这个链路看上去简单但每个环节对客户端的要求都不低。扫码开锁要求App在前台响应足够快冷启动慢半拍用户可能就转身去骑另一辆车了。骑行轨迹要求定位准确同时不能把电量耗尽否则用户骑到一半手机没电体验会非常差。地铁口、地下车库、商场等弱网场景下用户可能扫码成功却收不到开锁结果这时客户端就需要做超时重试、请求幂等和状态恢复。所以试卷里出现内存管理、多线程、网络和界面性能的题并不是跟着主流面试题库抄的而是这些技术点恰好对应了摩拜App最核心的体验指标。理解这一点你再回头看这份卷子每道题都会变得鲜活起来。1.2 考点分布内存、并发、UI性能形成的三角区我记得当年拿到卷子后先把所有题目粗略扫了一遍大概能够摸清它的出题逻辑。整张卷面围绕三条主线展开内存管理、多线程并发、UI性能中间再穿插runtime、网络、架构设计类题目形成一套完整的评估框架。考察主线常见出题角度对应真实业务场景内存管理ARC原理、循环引用、copy修饰符页面返回后能否及时释放避免长期运行Crash多线程并发GCD、锁、读写竞争、原子性定位数据上报和UI刷新并发执行避免主线程拥堵UI性能卡顿、离屏渲染、列表复用、事件响应骑行轨迹页面滚动、扫码动画流畅度runtimeKVO、KVC、消息转发埋点、数据绑定、热修方案的底层基础网络方案超时重试、弱网策略、缓存地铁口弱网环境下的开锁请求可靠性设计方案架构分层、模块解耦、状态管理扫码开锁、轨迹上传等核心业务的可维护性这种出题方式其实是在筛人。它要刷掉两类人一类是只会调用API、对底层机制完全无感的“接口拼装师”另一类是没有工程意识、只在本地写Demo的“单向学习者”。反过来说如果你对系统的运行机制有真实的理解并且做过一些性能优化、弱网处理、状态管理哪怕不背摩拜的题库也能在这些题上拿到不错的分数。2. 高频考点复盘每道题背后的出题意图与踩坑点2.1 copy与block修饰符一句话答完等于没答我记得卷子里有一类基础题看着简单实际上特别容易答浅。比如问NSString属性应该用strong还是copy很多人的第一反应是“用copy防止被外部修改”。这话没错但只是第一步。面试官真正想听的是你知不知道NSMutableString和NSString之间赋值时的“动态类型风险”。// 错误示范 property (nonatomic, strong) NSString *name; // 正确示范 property (nonatomic, copy) NSString *name;如果把一个NSMutableString赋给一个声明为strong的NSString属性原对象的可变特性并不会消失。外部一旦修改了那个NSMutableString属性的值也会跟着变因为两者指向的是同一个内存对象。而用copy修饰后编译器会执行一次不可变拷贝把属性指向一个全新的不可变字符串外部怎么改都影响不到它。这就是copy在字符串、数组、字典属性上的核心价值。进一步说block属性用copy修饰也是同一个逻辑。在MRC时代block默认创建在栈上函数返回后栈上内存可能失效所以需要copy到堆上持有。ARC下编译器会做很多自动处理但为了语义明确和向后兼容客户端代码里还是建议显式写copy。如果答题时能把这些层次讲清楚再顺手提一下“不可变字符串和可变字符串在深浅拷贝上的差异”这道题基本就过关了。怕的就是只答“防止被修改”几个字缺乏场景感。2.2 NSTimer循环引用不能只在dealloc里invalidateNSTimer循环引用是iOS面试的经典题目摩拜这份卷子也考了。它为什么经典因为它不是纯理论题而是项目里真的会遇到的问题一个ViewController里创建了一个重复执行的定时器页面关闭后控制器却一直不释放导致内存持续上涨。先看最常见的错误写法self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:selector(fire) userInfo:nil repeats:YES];这条代码会形成一条引用链RunLoop持有TimerTimer持有targetself而self又把timer存放在自己的属性里。结果是ViewController被Timer间接持有整个引用环无法打破dealloc永远不会执行。很多人在dealloc里写了一条invalidate问题是dealloc根本不会被触发这个invalidate就是永远不会执行的“安慰剂”。正确的处理思路是在合适的时机主动终止定时器比如viewWillDisappear或didMoveToParentViewController里调用invalidate。iOS 10以后可以用带block的Timer API配合weakSelf来避免Timer long-term持有self__weak typeof(self) weakSelf self; self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *timer) { [weakSelf fire]; }];但这并不是一劳永逸。block版本的Timer虽然解决了循环引用Timer本身还是被RunLoop持有如果不在合适的时机invalidate它依然会占用资源、持续回调。所以在回答这道题时我会把“解决引用环”和“释放定时器资源”分成两个层次分别给出方案。能答到这一层说明你真的在处理过类似问题而不是背过一篇博客。2.3 KVO与KVC底层两个最容易懵的运行时问题KVO这块考的不只是“怎么注册观察者、什么时候移除”而是它的底层实现。你给某个对象的属性添加KVO后Runtime会动态创建一个该类的子类命名大概是NSKVONotifying_XXX然后把这个对象的isa指针指向新的子类。子类里重写了被观察属性的setter方法在赋值前后分别调用willChangeValueForKey和didChangeValueForKey这样才能在属性变化时通知观察者。验证方法很简单。注册KVO前后分别打印对象的class方法你会看到类名发生变化。很多人听到这里会惊讶但其实这正是iOS动态性的体现。如果答题时能补充一句“KVO通知是同步触发的所以不要在改变属性时做耗时操作否则会阻塞当前线程”会显得更有经验。KVC的底层查找顺序也常常被放在同样的题目里。访问对象的某个key时系统会按顺序查找setter/getter方法再查找实例变量如果都没有则调用valueForUndefinedKey或setValue:forUndefinedKey:默认实现是抛异常。这个机制在做字典转模型、运行时埋点时会用到。复习时如果能把KVO、KVC、isa、method swizzling这条线串起来理解就不怕面试官顺着一个点追着问到底。2.4 GCD组合与多读单写四种搭配背后的线程模型GCD是笔试里最常考的多线程知识点因为它覆盖了线程管理、队列设计、并发控制很多内容。基础题目可能会让你解释sync/async与串行/并发队列的搭配效果我建议用一张表去记忆。组合方式执行特点容易踩的坑同步 串行队列按顺序执行不开新线程阻塞当前线程在串行队列里同步派发新任务到自己立即死锁同步 并发队列任务会开新线程并发执行但当前线程会等待全部完成并发队列上同步派发、等待自己同样可能死锁异步 串行队列任务按顺序执行但不阻塞当前线程同一个串行队列内部做递归派发容易堆积异步 并发队列任务并发执行不阻塞当前线程多任务同时写同一个资源出现数据竞争比较高频的进阶题是“多读单写”。多个线程同时读取一块数据是安全的但只要有写操作就必须保证同一时刻只有一个线程在写。最优雅的做法不是加一把大锁锁住所有读写而是用GCD的dispatch_barrierself.concurrentQueue dispatch_queue_create(com.example.rw, DISPATCH_QUEUE_CONCURRENT); - (id)readData { __block id result; dispatch_sync(self.concurrentQueue, ^{ result self.data; }); return result; } - (void)writeData:(id)newData { dispatch_barrier_async(self.concurrentQueue, ^{ self.data newData; }); }注意这里必须是自定义的并发队列全局并发队列不能保证barrier只针对你自己的资源用了反而会影响系统其他任务。如果笔试里能写出这个区别说明你在真正使用过GCD而不是只背了概念。另一个常见的配套考法是dispatch_group和dispatch_group_notify用于处理多个并发请求全部结束后的统一回调在有批量上传、资源并发下载的场景里很实用。2.5 atomic与锁属性线程安全的边界到底在哪MOS的atomic属性在很多初学者眼里等于“线程安全”这其实是个很大的误解。我在项目里也见过新同事用atomic修饰可变数组成员然后以为高枕无忧。笔试里只要考到这个点基本就是要把这个误区拆穿。atomic的保证非常有限它只对getter和setter方法做原子操作保证同一时刻多个线程读写这个属性时不会产生“读了半截值”的情况。但它不保证整个业务过程的线程安全。举个例子假设一个属性是NSMutableArrayatomic只保证拿到这个数组的getter操作是原子的但两个线程同时向这个数组里addObject依然会造成数据竞争和崩溃。因为addObject根本不是getter/setter层面的事。所以在真实工程里如果要对可变容器做并发写要么用串行队列串行化操作要么用锁保护整段操作。锁的选择也很有讲究在低竞争场景下pthread_mutex明确清晰如果是高频访问且临界区很小可以考虑自旋锁的思想但现代iOS里os_unfair_lock是更稳妥的选择不要在一开始就无脑上NSLock更不要用synchronized包住一大块逻辑性能差且容易误伤。回答atomic这道题时比较高级的答法是用一个实际例子说明“atomic 可变对象 ≠ 线程安全”然后顺势抛出自定义并发队列实现多读单写的方案。这样这道题就从“概念题”变成了“方案题”。3. 方案设计题地面轨迹与扫码开锁背后的工程取舍3.1 骑行轨迹采集从定位参数到批量上报的完整链路方案设计题是拉分项。摩拜这类有强位置属性的App一定会问到轨迹或者地图相关的东西。很多人一上来就想复杂的算法结果连最基本的定位参数都没说清楚反而失分。骑行轨迹的采集链路通常要经历定位权限申请、定位参数配置、原始坐标过滤、坐标系转换、本地缓存、批量上传、服务端抽稀展示。每个环节都有取舍。定位参数是最容易忽略的第一层问题desiredAccuracy设得越高耗电越大距离过滤器distanceFilter设得越小回调越频繁流量消耗越大。骑行场景里用户车辆移动速度通常在10-25km/h不需要每秒都回调定位用kCLLocationAccuracyHundredMeters配合10到20米的distanceFilter就足够还原轨迹还能明显省电。坐标过滤是第二层关键点。手机GPS在市区、高架、地道里会出现漂移直接拿原始坐标上报会在服务端画出一条“飞檐走壁”的轨迹。工程上可以先做速度校验剔除那些明显超出骑行速度范围的跳点再用卡尔曼滤波或移动平均平滑坐标。这个话题如果你能说出“在真实骑行数据里过滤后轨迹里程和地图计费里程的误差能从百分之十几降到百分之四左右”这类经验面试官会立刻把你和其他背书的人区分开。坐标系转换也是必须提的点。国内地图服务商普遍使用GCJ-02坐标系而系统定位返回的是WGS-84坐标两者之间隔着一次偏移转换。在做轨迹点上报和地图绘制时如果坐标系统一标准没定对所有轨迹都会整体偏移几十到几百米。这个坑我在实际项目里遇到过所以每次跟新人讲轨迹方案都把坐标基准放在最前面。最后是上报策略。骑行过程中网络可能断断续续所以不能每收到一个GPS点就立刻发起请求。通常会把轨迹点先落到本地数据库或文件里骑行结束后在Wi-Fi环境或App回到前台时统一上传并用订单号做分段或全量增量同步。这样既保证可靠性又不浪费用户流量。3.2 扫码开锁状态机、超时重试与幂等设计扫码开锁这个功能看起来就是“扫一下然后解锁”但笔试里一旦要求设计方案它考察的是你对复杂业务的控制能力。开锁不是一次单纯的网络请求而是涉及二维码信息解析、检查车辆状态、请求服务端下发指令、通过蓝牙或其他通道把指令下发到车锁、等待开锁结果回执的完整过程。客户端在中间扮演调度者的角色。设计这个流程时第一件事不是写网络请求而是定义状态机。我见过很多人在状态管理上吃亏就是因为没有明确的业务状态导致按钮被连点、开锁结果和用户操作互相覆盖。最简单的状态可以定义成这样状态含义触发条件idle待扫码初始状态进入扫码页unlocking开锁中已请求服务端用户点击“开锁”按钮unlocked开锁成功收到成功回执unlockFailed开锁失败收到失败回执或超时timeout开锁超时超过预设时间未收到回执进入unlocking后界面上要锁定按钮防止重复点击。同时启动一个超时计时器比如10到15秒。一旦收到成功回执进入unlocked状态正常跳转骑行页如果超时则进入timeout状态提示用户重新尝试或联系客服。这里还有一个很容易忽略的幂等问题用户因为没收到回执手动重试了一次结果服务端其实已经下发过开锁指令这时如果不去重就可能出现重复开锁或扣费异常。所以客户端每次请求都要带一个唯一的业务请求ID服务端用它做幂等判断。答题时能把这个点主动说出来说明你有真实服务端协作经验而不只是会写UI。3.3 极端场景下的多级缓存方案摩拜的App在弱网环境下面临的压力比一般App大得多。用户在地铁口、地下车库、商店内部扫码开锁网络往往很差。如果整套逻辑必须依赖实时网络体验会非常糟糕。所以方案设计题里缓存策略很容易成为加分项。缓存不能只靠一种手段。内存缓存速度快但生命周期短文件缓存适合大对象但读取慢数据库适合结构化数据但不适合高频纯读写。合理的设计是多级组合字典或NSCache保存最近一次扫码记录保证App内存存活期间再次进入页面可以秒开SQLite或者文件系统保存最近若干条订单、用户信息和状态记录网络层再配合ETag或Last-Modified做资源增量更新。我记得当年做笔试时给缓存方案画过一版“四级结构”第一级内存对象缓存解决同一页面往返时的瞬时读取第二级轻量偏好存储保存少量用户配置和最近状态第三级数据库缓存存储历史轨迹和业务记录用于离线展示第四级文件系统缓存地图瓦片、图片和开屏资源。答题时如果能把每一级缓存的使用场景和淘汰策略讲清楚面试官会认为你具备整体架构意识而不是只能说出“用一下NSCache”。4. 笔试答题里那些没说透就丢分的细节4.1 手写代码题先保证写完再保证优雅不管是摩拜的笔试还是其他公司的笔试手写代码都会占一部分分值。很多人在平时阅读时觉得自己都会一动手就开始暴露问题。最典型的表现是纠结于最优解法结果连一个能跑的方案都没写出来。我记得卷子里有一道线程安全计数器的题很基础但特别能看出一个人的工程素养。初级写法一般是用synchronized包住计数逻辑优点是简单缺点是大范围锁住self可能影响其他需要访问self的代码。中级写法是用pthread_mutex或NSLock保护临界区锁的范围缩小到只有计数操作。更高级的写法是用os_unfair_lock或原子操作甚至用并发队列的barrier实现读写的并发控制。笔试答题时不要一上来就追求最复杂的方案先把一个能正确运行的版本写清楚再补充“如果是我在项目里做我会怎么优化”。这样既展示正确性也展示工程思考。另外手写代码时要注意细节变量命名是否清楚边界条件有没有考虑比如数组为空、参数为nil、重复调用时会不会产生副作用。很多面试官不会全量看你的代码能不能跑而是看你的代码习惯。一个能把普通计数器写得干净、防御性强的人比一个背了高级锁API但变量全部叫temp的人分数要高得多。4.2 答案要有验证痕迹这一点是我后来参加了很多面试、也坐在面试官那侧之后才体会到的。答题时最珍贵的不是“我知道正确答案”而是“我验证过这个结论”。同样的题目两个人都答出了NSTimer循环引用但只有一个人会在答案结尾说“我一般会在dealloc里打印日志再用Instruments的Leaks或者Memory Graph验证页面返回后控制器有没有释放。”这个差别非常大。验证痕迹意味着你不在“背题”而是在“用题”。像KVO这样看似理论的内容你可以补充一句“我在控制台打印过注册KVO前后的class方法确认类名变化”。像atomic的线程安全边界你可以说“我用多个线程并发写一个atomic修饰的字典实测仍然会崩溃”。这些实证经验比任何教科书结论都有说服力。平时学习时如果能养成动手验证的习惯笔试和面试时都会比别人多一层竞争优势。4.3 控制节奏把方案题留够时间摩拜这份卷子的题量并不小。我当时的策略是先做有把握的基础题卡壳超过5分钟就果断跳题把整张卷子的题目都过一遍再回头啃难题。方案设计题往往在卷子的后半部分分值高、思考量大绝对不能因为前面的题花太多时间而导致最后只能匆匆写几行。基础题重在准确方案题重在结构。方案题没那么看重标准答案而是看你能不能把事情拆解成模块能不能讲出每个模块之间的数据流和边界。所以答题时不要害怕长篇大论但每段话都要落在具体方案上比如状态机定义、缓存分层、重试策略而不是“我认为要做好用户体验”这种空话。时间分配上我建议基础题控制在总用时的40%以内方案设计题留出至少30%的时间。5. 考完摩拜后我如何把这份卷子变成复习框架5.1 把考点归入能力模型而不是背题摩拜这份卷子给我带来的最大影响不是让我背会了几道题而是逼我把iOS知识体系重新整理了一遍。考试结束后我花了整整一个周末把卷子里的考题按能力方向归档内存管理、并发编程、UI事件与性能、网络与缓存、runtime与动态性、工程与架构。归档后的复习表比“iOS面试题大全”有价值得多因为每一类能力都能对应到真实工作里的具体项目。比如UI事件这块卷子里问到的事件传递和响应链后来我在处理地图标注图层遮挡手势的问题时真的用上了。再比如网络与缓存这块卷子里反复出现的弱网重试和幂等思路后来我在做支付结果确认时也一直在用。如果把知识点放到场景里去学就不会觉得它是孤立的面试八股文而是你解决真实问题的工具箱。5.2 用“追问三个为什么”把简单题变成深水区我还养成一个习惯每复习一道题都逼自己连续追问三个“为什么”直到问不住为止。比如“为什么NSString属性要用copy”追问下去会到“深拷贝和浅拷贝的区别”“NSMutableString为什么是NSString的子类”“ARC什么时候做自动拷贝”这一层再往下还能追到“纯Swift的值类型是不是天然规避了这类问题”“Struct与Class的选择依据”。这种方法能很快暴露自己的知识盲区。我之前一直觉得自己懂GCD直到追问“为什么dispatch_barrier只能在自定义并发队列上确保多读单写”才意识到自己以前确实没有想过全局并发队列的干扰问题。当时我就把这个问题写成复盘笔记后来在好几个项目中都用到了。如果你现在也在准备笔试或面试可以试一下这个方法把手里已有的面试题逐条追问直到每道题都能从原理讲到落地场景那无论是摩拜还是其他团队都不会太难。说到底一份笔试卷能筛掉的是“没准备过的人”但真正能帮你在面试里走得更远的是你是否真正理解了iOS系统在每个环节的设计思路以及你能否把这些理解转化为一个可维护、可扩展的工程方案。希望这次复盘能让你少走一些我当时走过的弯路。
返回列表