ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多年后回看2018年欢聚时代iOS笔试题:内存、运行时与多线程

多年后回看2018年欢聚时代iOS笔试题:内存、运行时与多线程 多年前的校招笔试放到今天来看恰恰是检验一个iOS开发者底子是否扎实的试金石。欢聚时代2018年这套IOS A卷我印象很深它不怎么考偏门API也不靠复杂算法刁难人反而把大量篇幅放在内存管理、运行时、多线程和UI事件传递这些“日常写代码天天碰但很多人只是半懂”的地基上。这篇文章不是简单贴一份回忆版答案而是把每类题背后的考察意图、答题时应该展示的思考深度以及这些知识点在真实业务里的映射逐一拆开讲清楚。无论你是正在准备面试的应届生还是想自查短板的在职开发都值得对一遍。1. 2018年的这份IOS卷到底在筛选什么样的人1.1 从笔试题反推岗位能力模型看一份笔试卷最忌讳的是只盯题目本身先要琢磨出题人想要什么样的人。欢聚时代是做直播和IM起家的这类业务对iOS端的要求非常明确App常年跑在用户手里网络状况复杂消息收发频繁界面层级多还要保证内存不爆、界面不卡。所以整套卷子实质上是在筛选三种能力第一是底层的语言和框架理解第二是碰到复杂问题时的排查思路第三是工程落地时的细节意识。很多人复习iOS笔试喜欢背面试题大全什么“block用copy还是strong”“weak为什么自动置nil”背得滚瓜烂熟。但这份卷子给我最大的感觉是它喜欢把知识点嵌套在具体场景里。比如内存管理的题目不会直接问你ARC规则而是给你一段互相引用的代码让你分析是否泄漏多线程的题目会描述一个直播弹幕刷新的场景让你设计方案。这种出题方式更接近真实开发因为你在工位上遇到的需求从来不会是“讲一下GCD”而会是“这个页面滑动为什么卡”。1.2 这份卷子的整体难度与答题策略从难度梯度看这份卷子大概是这样的结构开篇的基础选择题覆盖Objective-C语言特性和Foundation框架属于送分题但要拿全也不容易因为会埋几个“看起来对、实际错”的选项中段的代码分析题直接上内存管理和运行时后段的简答题开始涉及多线程、网络和UI渲染最后的开放设计题则完全考察架构能力没有标准答案。答题策略上我后来复盘时最大的体会是不要只写结论要把推导过程写出来。比如判断一个对象是否泄漏你光写“会泄漏”拿不到什么分你要写清楚引用关系图、谁强引用了谁、在什么时机打破引用才合理。校招笔试的阅卷人通常不是简单对答案而是看你有没有完整的分析链条。即使某个结论判断错了只要前面的分析逻辑站得住也能让面试官看到你的思考能力。反之只给结论不给过程在开放题上基本等于放弃。2. 内存管理每一道引用计数题背后都是内存泄漏的教训2.1 引用计数与weak的实现原理iOS笔试对内存管理的考察从2018年到今天几乎没有变过因为这块确实是事故高发区。引用计数Reference Counting这套机制脑子里必须有一张完整的图对象创建时 retainCount 为1retain 加1release 减1减到0时系统调用 dealloc 回收内存。ARC 只是编译器帮你插入了 retain/release 调用并没有改变底层逻辑。weak 是另一个高频考点。很多人知道 weak 修饰的属性在对象释放后会自动变成 nil但如果只答到这里在笔试里只能算及格分。进一步要说明的是weak 之所以能自动置 nil是因为 runtime 维护了一张全局的 weak 表它是一个哈希表key 是对象的地址value 是 weak 指针地址的数组。对象释放时runtime 会根据对象的地址去这张表里找到所有 weak 指针逐个置为 nil。这个过程发生在 dealloc 的哪个阶段也是面试官喜欢追问的细节。我当时在卷子上画了一张对象生命周期图标出引用计数变化、weak指针的清理时机、以及 autoreleasepool 的介入点。这种图不需要多精致但能展示你是真的理解而不是背概念。后来我做项目排查内存泄漏时发现纸上能画清引用关系的人写代码时对对象生命周期的敏感度普遍高很多。2.2 循环引用的三种经典场景与检测手段循环引用是笔试题里最常出现的代码分析题也是线上问题的大户。常见的三种场景delegate 用 strong 修饰、block 内直接引用 self 或成员变量、NSTimer 对 target 的强持有。前两种大家熟悉第三种最容易漏。NSTimer 循环引用的经典套路是控制器强持有 timertimer 又强持有 target也就是控制器自己如果不主动 invalidate这条环永远解不开。很多人以为用 weak 修饰 timer 属性就能解决实际上没有用因为 timer 是被 RunLoop 强持有的你 weak 掉属性只是让自己访问不到它环依然存在。核心解法是保证 timer 在合适的时机失效或者用 block 形式的 timer API 配合 weakSelf。这个细节在笔试里很能拉开差距。还有一类题是把循环引用藏在看似无关的代码里比如在一个 block 里调用了 self 的私有方法而私有方法里又访问了 self 的属性表面上没写 self实际上 block 捕获了 self。iOS 13 之前直接访问成员变量不会产生编译器警告很多人就这么不知不觉写出了一个循环引用。这类题考察的就是你对 ARC 捕获规则的理解深度。答题时最好把 block 捕获变量的三种类型——局部变量、__block 变量、对象变量——分别解释一遍再指出当前题目属于哪种情况。2.3 从ARC到自动释放池面试官想听什么自动释放池AutoreleasePool在笔试题里出现的频率不低但大多数人只背了一个结论主线程 RunLoop 在每次循环末尾会释放自动释放池。实际上考官想听的是更细致的东西。我当时的回答思路是这样的先说明 autorelease 的本质是把对象注册到当前的 pool 中延迟 release 的时机再说明在 ARC 下方法返回值、__weak 变量的读取等场景都会自动产生 autorelease 对象最后补充一个实战细节——大量临时对象在一个循环里创建如果没有及时加 autoreleasepool内存峰值会明显上升因为对象要等外层池子释放才被清理。顺着这个思路可以把题目从“什么是自动释放池”引到“为什么 for 循环创建大量图片对象时内存暴涨”。我记得当时卷子上有一道题给了两段创建对象的循环代码问内存占用为什么不同。这就是典型的延伸考察概念场景优化方案。如果只答第一层后面的分就白丢了。3. 运行时机制消息转发是iOS动态性的灵魂3.1 从objc_msgSend到完整消息转发流程Runtime 是 iOS 笔试里最让应届生头疼的部分因为它抽象、看不见摸不着却贯穿了整个 Objective-C 的动态特性。核心入口是 objc_msgSend 函数方法调用在编译期并不会确定到底调用哪个实现而是等到运行时通过 isa 指针找到类对象再沿着继承链去方法列表中查找。找不到就去缓存中找再找不到就进入消息转发流程。消息转发有三个阶段动态方法解析resolveInstanceMethod、快速转发forwardingTargetForSelector、完整转发methodSignatureForSelector 和 forwardInvocation。笔试里常考的是问你“有哪些方式可以让一个对象处理它没有实现的方法”这就是要你把三个阶段都写出来而不是只提一两个。这个知识在真实业务里的应用很多。比如我很早以前做过一个数据打点方案利用消息转发把未实现的统计方法统一拦截下来转成日志上报避免在每个页面手动埋点。这套方案的基础就是 forwardInvocation把无法响应的 selector 包装成 NSInvocation统一处理参数和调用环境。笔试时如果能顺手举一个类似的例子阅卷人对你的工程能力会高看一眼。3.2 Method Swizzling的正确姿势与坑Method Swizzling 是运行时考察的进阶题也是最容易答出破绽的题。原理不复杂交换类的方法列表中两个 method 的 IMP 指针让原来的 selector 指向新的实现新的 selector 指向旧的实现。但面试官真正关心的是你会不会用以及知不知道坑在哪。正确的 swizzling 姿势应该包含几个要点在 load 方法里做而不是 initialize因为 load 是线程安全且只会调用一次要用 dispatch_once 保证只交换一次防止重复交换导致方法调不到交换的是类方法时要把 object_getClass 传进去而不是直接传类对象因为类方法存在元类里。我见过太多网上流传的代码示例只写了核心交换那一行没有处理这些边界情况。笔试里如果出了 method_exchangeImplementations 的题目你把这些细节写全基本就是满分答案。更进阶一点还可以提一下新版本系统对 swizzling 的兼容处理以及为什么很多性能监控 SDK 不推荐用 swizzling 来做全量 hook——因为它会影响整个 App 的消息发送性能而且覆盖范围太大容易误伤系统行为。3.3 关联对象与动态添加方法运行时还经常考关联对象AssociatedObject和动态添加方法class_addMethod。关联对象在分类中添加属性时至关重要因为分类本身不能直接添加实例变量。笔试题可能会问你关联对象的释放时机。很多人不知道关联对象是在 dealloc 时通过 objc_removeAssociatedObjects 来处理的而且关联对象在对象释放后也不会自动置 nil需要你手动处理。动态添加方法这道题最经典的场景是给一个空类动态添加一个方法实现。答案一般包含两步用 typeEncodings 描述方法签名用 class_addMethod 把函数指针绑定到 selector 上。typeEncodings 的写法是很多人容易写错的把参数和返回值编码搞混。我当时的记忆方法是默认方法都隐含 self 和 _cmd 两个参数所以方法编码一定是“返回值:参数列表”这样的顺序。这个细节写进答案里能显得你代码功底非常扎实。4. 多线程与并发GCD的串并行陷阱与锁的取舍4.1 队列与线程的对应关系多线程在iOS笔试题里的出题深度逐年递增但核心还是围绕 GCD。首先要分清队列和线程不是一回事串行队列上的任务按顺序执行但线程本身可以切换并行队列上的多个任务可以被系统分配到不同线程上并发执行。实际上系统为了降低开销也不会为每个任务创建一个新线程而是维护一个线程池。理解了这层很多“离奇”的现象就有了解释。比如你向一个串行队列提交两个耗时任务第一个任务卡住了第二个任务什么时候执行答案是永远等下去因为串行队列同一时刻只能执行一个任务。但如果你在不同线程上打印当前线程号会发现串行队列的两个任务偶尔不是同一个线程这就说明了线程池的存在。笔试里的判断题经常会在这里做文章说“串行队列一定在同一个线程上执行”这是错的。同步派发dispatch_sync和异步派发dispatch_async的区别也是高频题。sync 会阻塞当前线程等价于在当前线程上插队执行所以特别容易引发死锁async 不会阻塞系统会把任务放到目标队列上调度。我在答题时会把四种组合串行/并行 × 同步/异步列成一张小表分别说明执行的线程和阻塞情况这样逻辑非常清晰阅卷人看起来也舒服。4.2 死锁的成因与避免经典的主队列陷阱几乎每一份iOS笔试题都会有一道关于主队列死锁的题目。典型代码是在主线程上执行 dispatch_sync(dispatch_get_main_queue(), block)瞬间死锁。原因分析要说清楚主队列是串行队列当前已经在主队列上执行任务sync 又要求在当前线程上等待新任务执行完而新任务排在主队列的末尾要等当前任务结束才能开始于是互相等待。但很多题目不会只考这一种死锁会考嵌套队列。比如在一个串行队列 A 里 dispatch_sync 到另一个串行队列 BB 里又 dispatch_sync 回 A这就是交叉等待同样会堵死。还有用信号量dispatch_semaphore_wait在主线程上等待网络回调也是一个经典陷阱如果回调本身在主线程执行信号量会把主线程锁死回调永远进不来。答这类题的关键是把“谁在等谁”的关系讲清楚不要只背结论。我一般画一个等待关系链任务1等任务2任务2等任务1形成闭环就是死锁。这个分析方式不仅笔试有用后来定位线上卡死问题时同样好用。你只要能在崩溃日志里找到主线程等待的调用栈沿着这个思路往下追基本都能找到是谁在互相等。4.3 从OSSpinLock到os_unfair_lock锁的演进说明什么锁的题目在笔试题里看起来是选择题实际上能挖出很多内容。早期代码里经常看到 OSSpinLock但如果问你现在能不能用正确答案是不能。因为 OSSpinLock 存在优先级反转问题持锁的低优先级线程可能被高优先级线程抢占导致高优先级线程自旋等待但低优先级线程迟迟得不到 CPU最后系统出现长时间无响应。苹果后来在 iOS 10 用 os_unfair_lock 替代了 OSSpinLock。os_unfair_lock 不是自旋锁它会等待线程进入休眠避免忙等和优先级反转。从考察的角度面试官看到你能说出 OSSpinLock 的问题就知道你不仅会用锁还了解过锁的实现原理。常见的锁还有 NSLock、NSRecursiveLock、synchronized、dispatch_semaphore各自适用的场景不同笔试里可能会给你一个多线程场景让你选择最合适的方案。synchronized 是最简单但性能最差的锁因为它隐式地使用了一个递归锁并且需要查表dispatch_semaphore 适合控制并发数比如限制同时下载的任务数量NSRecursiveLock 适合递归函数中加锁日常保护属性写入用 os_unfair_lock 或者简单的串行队列就够了。写答案时建议把每个锁的特性和适用场景各用一句话说明这样既能体现广度也能体现选型的判断力。5. UI与事件传递从hitTest到自动布局的完整链路5.1 事件响应链一个点击到底经过了什么UI相关的笔试题目里事件响应链几乎是必考的因为它是理解整个UIKit交互的基石。一次点击屏幕后系统先通过 hitTest:withEvent: 找到最合适的视图来响应然后事件会沿着响应链responder chain逐层传递给它的nextResponder如果最终没有人处理事件会被丢弃。hitTest 的过程要写清楚从 UIWindow 开始先判断当前视图能否接收事件isUserInteractionEnabled、hidden、alpha再倒序遍历子视图对每个子视图调用 pointInside:withEvent: 判断点是否落在视图范围内最后返回最上层的那个子视图。这个流程里有一个容易出错的地方hitTest 返回的是子视图调用链上最深的那个视图而不是第一个命中的视图因为它是递归的。笔试里常见的变体是问“点击一个遮挡了按钮的透明视图为什么按钮不响应”或者“怎么让一个超出父视图边界的子视图也能响应点击”。后者的答案通常有两个方向重写父视图的 hitTest 返回子视图或者重写 pointInside 扩大点击区域。这两个方案在答题时都要解释原理而不只是贴代码。我在实际项目中用最多的是扩大点击区域比如一个 20pt 的小按钮在移动端很难点中我通过重写 pointInside 把可点击区域扩大到 44x44这样既满足了交互需求也不影响视觉布局性能开销微乎其微。5.2 自动布局与渲染机制自动布局的题通常不会直接考约束怎么写更多是考你对约束体系和渲染机制的理解。常见问题包括两个视图等宽、按比例布局、UILabel 换行的约束设置以及 frame 和 Auto Layout 混用时的注意点。layouSubviews、updateConstraints 和 layoutIfNeeded 的调用时机是面试官很爱问的。这里有一个关键知识setNeedsLayout 和 layoutIfNeeded 不同前者只是打标记等到下一个 RunLoop 周期才更新布局后者会立即强制同步布局。如果想在约束生效后立刻拿到正确的 frame必须调用 layoutIfNeeded。很多人做动画时发现约束变了但视图没动就是因为缺这步。还有一层是 drawRect 和 Core Animation 的渲染流程。UIKit 的绘制最终会提交到 Core Animation 的提交事务中由渲染服务进程统一处理。笔试中如果问到优化滑动流畅度你能答出“避免在 drawRect 里做大量绘制、避免隐式图层动画在刷新过程中被打断、尽量用图层合成而不是频繁重绘”基本就能过关。如果你的答案里还能带出一个实际优化案例比如列表页用异步绘制代替主线程重绘那分数会更高。5.3 UITableView与UICollectionView的复用机制列表是iOS开发里绕不开的话题笔试中针对它的出题率也很高。复用机制的核心是 dequeueReusableCellWithIdentifier系统通过重用池来复用已经滑出屏幕的 cell避免频繁创建和销毁。看似简单但有一些坑是笔试题特别喜欢考的identifier 不匹配导致复用失败每次创建新 cell 导致内存暴涨或者 cell 上的子视图在复用时没有清理干净导致内容错乱。回答这类问题时除了描述复用机制最好还要提一下界面刷新的策略reloadRows 和 reloadData 的区别、beginUpdates 批量更新的作用、以及高频率刷新时如何做节流。我当年在直播项目中处理弹幕消息一开始每条消息来了都 reloadData手机直接卡成PPT。后来改成只刷新新增的那一行或者用 UICollectionView 的 performBatchUpdates 做批量处理流畅度才恢复正常。这个例子完全可以写进笔试答案里作为支撑。6. 网络与数据持久化笔试中的工程化考察6.1 HTTP与HTTPS的连接过程网络层在笔试题里比重大但很多应届生答得比较浅。比如问HTTP和HTTPS的区别很多人只答“HTTPS有加密”拿不到高分。正确的展开方式是HTTPS 在 HTTP 和 TCP 之间加了一层 TLS/SSL通过证书验证身份、协商对称密钥、加密传输数据。整个过程分为证书验证和密钥协商两阶段非对称加密用于交换对称密钥实际传输用对称加密因为性能好。TCP 三次握手和四次挥手也是基础中的基础但加分点在于能不能联系到iOS开发。比如直播业务里的弱网优化大家常提到的“HTTP/2 多路复用”解决的就是 TCP 并发连接数限制和队头阻塞问题。笔试问到网络优化时你能提到这些说明不是只会调 AFNetworking。DNS 解析和 CDN 缓存也是网络题里常见的延伸方向。App 首次启动时做接口请求往往会比较慢部分原因就是 DNS 解析耗时所以一些大型App会用 HTTPDNS 来绕过传统 DNS 的解析流程。这个知识点写进试卷里一下就能和普通开发者拉开差距。6.2 数据持久化方案对比与选型逻辑数据持久化这块面试官喜欢让候选人比较 NSUserDefaults、plist、归档、SQLite 和 Core Data。如果只是列优缺点会显得很浅。我通常的做法是先按使用场景分类轻量级的键值对用 NSUserDefaults结构化简单数据用文件存储或归档数据量大、查询条件复杂、需要关联查询的用 SQLiteCore Data 是对象关系映射框架适合中大型项目但有学习成本和调试成本。笔试中特别容易答错的是NSUserDefaults 不能存自定义对象除非转成 NSData归档是加密的还是不加密的——归档只是二进制序列化不是加密只要写清楚这一点就能排除一批人。SQLite 的题还会问事务和索引这属于数据库通用知识最好也准备一下。我记得当年这道题的陷阱在于有些方案看起来能用但根本没有考虑数据量大了之后的性能。比如说存档整个数组几百条的小数据没问题上万条就卡了。答题时把性能边界说清楚会让你的答案明显比背书的人更有层次。7. 开放设计题用IM消息架构展示你的工程视野7.1 这类题的考察意图与答题误区每年笔试最后都有一道开放设计题让考生设计一个功能模块欢聚时代这种以IM和直播为核心业务的公司大概率会把设计和消息通讯沾边。这里最常见的误区是把这道题当成“实现一个聊天”来写大段贴数据库表结构和Socket连接代码。阅卷人想看的其实是你面对一个模糊需求时如何把问题拆解成模块、如何权衡取舍、如何考虑扩展性。我当时写这道题时先列了一批问题来定义范围支持单聊还是群聊、消息类型有哪些、是否需要离线推送、多端同步怎么处理、消息顺序怎么保证、已读回执要不要做。把这些约束条件写清楚后再往下拆模块就会显得你的设计是有依据的而不是拍脑袋写的。7.2 一个可参考的答题框架消息分发与UI解耦一个比较稳妥的答题框架可以从数据流、UI更新、容错策略三个维度展开。数据流方面Socket 收到消息后统一转成模型投递到一个消息管理器中由管理器负责写库、排序、去重、缓存。UI 更新方面ViewModel 监听消息管理器的变更通过闭包或代理通知界面刷新避免控制器直接持有 Socket 和数据库。这个设计里有一个细节特别加分消息去重。真实IM场景中由于客户端重连、服务器重推同一条消息可能到达多次如果没有一个消息ID去重机制用户会看到重复消息这是一种典型的“看起来简单做起来麻烦”的问题。你能主动提到消息ID的生成规则、本地去重的缓存策略阅卷人会认为你有实际项目经验。另一个加分点是弱网和发送失败的队列设计。消息发送失败后不能直接丢要有一个可重试的发送队列并且要处理好消息的发送中、发送成功、发送失败三种状态展示。这种细节最见功底比堆砌技术名词有用得多。7.3 架构设计中的工程化思维开放题想拿高分必须在答案里体现工程化思维而不只是“能用”。我一般会再补充三块内容模块之间的依赖关系怎么控制比如消息管理器不直接依赖网络层而是通过协议抽象多线程安全怎么保证比如消息模型的写入队列和读取队列如何隔离扩展性怎么设计比如以后要加短视频消息、语音消息能否在不改核心逻辑的前提下新增处理模块。这套思路在笔试中的应用类似于你在给一个新同事讲业务架构时会先从职责边界讲起再讲数据流转最后讲异常处理。阅卷人本来就是资深开发者他看到你把容错、扩展性、线程安全都考虑进来自然会判断你对软件开发的理解不是停留在API层面。我记得我在回答最后画了一张简洁的模块依赖图并且声明“箭头表示依赖方向不表示调用时机”。虽然是笔试题但用这种工程文档的表达方式会让你的答案看起来非常专业。不过这里提醒一下笔试时如果画图一定确保结构清晰不然会适得其反。8. 时隔多年再看这份卷子基础题的真实分量几年后再回顾欢聚时代2018年这套IOS笔试题我有一个挺强烈的感受当年觉得不过是考试的知识点后来全都在生产环境的崩溃日志和性能瓶颈里重逢过。内存问题导致OOM消息转发被做成了AOP埋点的基础多线程死锁让线上用户卡死UI响应链理解不透导致诡异的不响应事件这些不是一个一个孤立的题目而是每一个iOS开发者会持续面对的日常。如果你是在准备笔试或面试不要只把答案背下来试着把每个知识点和自己写过的代码、踩过的坑对应起来。比如你曾经为了一个timer不释放而头疼那就去理解NSTimer的强引用链条你曾经发现列表滑动掉帧那就去理解cell复用和异步渲染的原理。这样复习知识点哪怕两年后不做笔试题了这些理解也会留在你的肌肉记忆里帮你更快地定位问题。如果时间有限我建议优先把内存管理、运行时、多线程和消息传递这四个模块吃透它们是iOS开发里最底层的几块基石也是换任何业务场景都跑不掉的东西。至于那些具体的API用到的时候看文档完全来得及但底层的原理一旦通了上层的东西学起来就是一条线的事。
返回列表