ARTICLE DETAIL

资讯详情

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

2023春招iOS笔试复盘:底层考点与答题策略

2023春招iOS笔试复盘:底层考点与答题策略 2023年小满春招第二批iOS研发岗笔试我前后帮好几个朋友做过复盘自己也专门把题目拆开重新做了一遍。今天把这些观察整理出来希望能给正在准备iOS面试的人一个清晰的路标。这套笔试覆盖了OC底层的Runtime和内存管理也涉及并发、RunLoop、网络层、架构设计、性能优化乃至Swift混编既有纯记忆点也有需要现场推演的场景题。无论你是应届生还是工作两三年的开发者都可以拿它当一次自检。为了不剧透原题我按考点模块来拆尽量还原当时我看到的答题思路把每类题背后的考察意图和候选人的常见失误都讲清楚。内容偏底层但我会用尽量直白的方式说明白边讲边给实操建议。1. 这批笔试的整体考察思路与设计侧重1.1 第二批笔试的定位差异春招笔试通常不止一批第一批往往承担“广筛”的任务题目会偏基础、偏概念判断只要数据库、网络、iOS基础有大问题就被刷掉。到了第二批竞争池已经收缩了很多出题人更关注的是“有没有深度理解能力”和“能不能在真实现场解决复杂问题”。我在复盘这套题时明显感觉到硬背八股文是过不去的。比如某个题看起来在问NSDictionary的底层结构但再往下追一层就会牵扯到哈希表实现、isEqual:和hash的关系、可变与不可变容器的存储差异。如果只会背“NSDictionary用哈希表实现”到第二个追问就卡住了。这种答法在第一批也许能混过去在第二批很危险。另一个特点是题目之间会互相呼应前面某道题留下的信息后面场景题会再次用到。这要求你答题时保持一致性。比如内存管理部分的答案会影响你对循环引用场景题的处理方式。如果前面说“weak能解决循环引用”后面却答不出weak在什么场景下会失效批卷的人立刻能看出你的理解是碎片化的。第二批笔试对候选人的时间分配也有隐性的考验。题量不算少但分值并不均匀。我见过好几个候选人前面几道主观题写得很长从架构扯到音视频结果后面两道代码题只剩十几分钟连核心思路都没写完整。这在笔试里非常吃亏。1.2 考点权重与答题策略如果把这套笔试的考点按出现频率和分值排一下大概是这样的顺序考察方向出现形式推荐投入时间占比OC语言与Runtime概念题 代码推断25%内存管理与Block改错题 循环引用场景20%并发与RunLoop概念题 多线程场景题20%网络层与架构设计设计题 开放性作答15%性能优化与Swift指标题 混编题15%综合排查场景复盘5%这个占比说明底层基本功仍然是大头架构和网络虽然分值不低但更偏“表达与取舍”不需要写太多代码。所以答题策略上我建议先把语言、内存、并发这三块啃透再花时间整理架构题的答题框架。时间分配也很关键。拿到试卷先快速浏览一遍把会做的题在脑海里面标记好先做代码题再做开放性设计题。代码题是硬分数开放性题只要结构合理、表达清晰分差不会太大。我自己做这套卷子时是倒着做的先把后半部分的代码改错和场景题处理掉再回头写概念题。原因很简单概念题很多是纯记忆做完了容易被后面的时间压力影响心态。2. 核心考点拆解iOS语言与内存管理部分2.1 OC Runtime 底层题怎么答才不显得背题Runtime在iOS笔试里几乎必考。常见问法包括消息发送的流程是什么objc_msgSend做了哪些事isa指针的作用是什么class和meta-class的关系是什么这类题想拿高分不能只把流程背出来还要解释“为什么这样设计”。我的答题套路是先把流程一句话带过消息发送会先查isa指向的类再查方法缓存缓存没命中就查方法列表再向上查父类最终没找到会走消息转发机制。然后立刻补一段自己的理解说明这个流程其实是一种动态查找让对象在运行期可以改变行为这也是OC能实现Method Swizzling、KVO这些“魔法”的根本原因。再往下要能接住追问。笔试里有一道小题是问“objc_msgSend是直接调用还是递归查找”。正确答案是直接拿到IMP然后跳转这个过程已经由编译器生成的汇编代码完成了方法查找和跳转而不是调用函数去递归遍历方法列表。很多候选人在这里会犹豫说明平时只看了流程图没看底层实现。补充一个容易踩的坑objc_msgSend在x86_64和arm64架构下参数传递规则不一样stret结构体返回的场景也有差异。笔试不一定会问得这么细但如果你主动提到“不同架构下消息发送的签名不同”会让批卷人觉得你不是纯背诵而是真看过相关博客或文档。关于isa指针现在的实现已经是指针或非指针联合体了。非指针isa会把引用计数、weak标记等信息压缩到同一个64位空间里这就是为什么在老版本的Runtime源码里能看到isa_t这个联合体。答题时可以提一句isa不单纯指向类对象它还能携带对象的部分内存管理状态这是为了节省内存和提升缓存命中率。2.2 内存管理的高频变形题内存管理在第二批次笔试里出现得很刁钻不会直接问“ARC是什么”而是给你一段代码让你判断是否有内存问题。典型案例是这样的- (void)viewDidLoad { [super viewDidLoad]; NSMutableArray *array [[NSMutableArray alloc] init]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [array addObject:hello]; }); }问array会在block执行前被释放吗答案是不会。Block会对捕获的__strong对象做一次copy持有array在block内部被强引用所以它的生命周期会被延长到block执行完毕。但如果你在block执行前手动把array设为nil那就另说说明外部引用被断开了。这个点还可以延伸如果array是用__weak修饰的block内部捕获时不会强持有3秒后它可能已经释放了给一个已经释放的对象发消息会怎样这里就牵扯到野指针的排查思路了。内存管理部分还有一个高频变形题autorelease对象什么时候释放很多候选人背了“runloop循环结束时”但不够准确。ARC下一个autorelease对象通常会在当前autoreleasepool被drain时释放而主线程的runloop会在每次事件循环结束后自动drain一次。如果是在自己创建的autoreleasepool里那你得知道它可以手动控制释放时机。把这个逻辑链答出来比单纯背结论要好得多。关于内存问题的排查笔试里出现了一道类似“怎么定位循环引用”的题。最好的答案不只是“用Instruments的Leaks”而是结合LLVM的静态分析、Instruments的Leaks模板、debug时打印dealloc日志、以及使用Malloc Stack追踪堆栈。答出两三种手段并且能说明各自适用场景就能拿到不错的分数。2.3 Block 循环引用从原理到解法循环引用是iOS面试的“万金油”考点但在小满这批笔试题里它换了一层皮代码里既有self又有_ivar还有局部变量问哪种写法会产生循环引用。typedef void (^Block)(void); implementation MyClass { NSString *_name; } - (void)setupBlock { __weak typeof(self) weakSelf self; self.block ^{ __strong typeof(weakSelf) strongSelf weakSelf; NSLog(%, strongSelf-_name); }; } end这段代码整体上是安全的但它的安全程度取决于weakSelf是否被strongSelf承接。如果不承接在block执行期间self可能已经释放访问_name就是野指针如果承接了strongSelf在block执行期间保证了self存活退出block后释放不会形成新的循环引用。答这个题的关键是__weak打破循环__strong保证执行期安全两者是配合关系不是二选一。还有一类隐藏题self.block里捕获了父对象的属性self.parentView而不是self这样会循环引用吗很多人以为只要不写self就不会循环。实际上如果parentView是self的一个强引用属性而block又被self持有那么self - block - parentView - self依然形成环。笔试里这道题考的就是“不要把引用链条看得太浅”。3. 并发与RunLoop最容易问穿的模块3.1 GCD 与 NSOperation 的选择题并发这块最经典的问题是“什么时候用GCD什么时候用NSOperation”。笔试虽然不要求你写出完整的使用过程但你需要把选型依据讲清楚。我给的思路是如果只是“丢一个任务到后台执行完成后回主线程更新UI”GCD是首选简洁直接。但如果要做任务编排、依赖关系、取消操作、最大并发数控制NSOperationQueue更合适因为NSOperation本质是对任务和状态的封装支持addDependency、cancel、setQueuePriority这些能力。用GCD也能模拟依赖但需要借助dispatch_barrier或信号量写起来别扭还容易埋坑。另外一道典型的题是“dispatch_queue_create创建的队列是串行还是并发”。答案是通过dispatch_queue_create第二个参数传入DISPATCH_QUEUE_SERIAL就是串行传入DISPATCH_QUEUE_CONCURRENT就是并发如果传NULL默认是串行。这个点本身不难但它经常和“dispatch_get_main_queue和主线程的关系”混在一起问你要能区分清楚主队列是一个特殊的串行队列它绑定了主线程但串行队列并不一定运行在主线程。3.2 RunLoop 机制与线程保活RunLoop这部分的笔试题目问法是“如何让一个后台线程长期存活并接收任务”。很多候选人第一反应是“用performSelector:onThread:”但答不出后台线程的RunLoop需要手动开启。后台线程默认不自动开启RunLoop所以你想让它反复接收任务得自己启动- (void)threadEntryPoint { autoreleasepool { NSRunLoop *runLoop [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }run方法是永久运行只有收到停止通知才会退出。如果你在业务代码里看到“子线程的RunLoop必须add一个source或timer才会跑起来”这个说法不完全准确run确实会尝试运行但因为没有输入源或timerrunloop会直接返回造成线程无事可做。所以为了让它“活着”必须注册一个NSMachPort或者添加一个timer。答题时可以延伸一下RunLoop的几种运行模式。NSDefaultRunLoopMode在发生界面滑动时会暂停UITrackingRunLoopMode用于跟踪触摸事件NSRunLoopCommonModes是一组可标记的模式集合。如果你在主线程用NSTimer刷新UI不加NSRunLoopCommonModes滚动的时候定时器会卡顿。3.3 多线程同步与资源竞争场景题这块的场景题很典型有多个网络请求并发回来需要等所有请求完成后再统一刷新UI怎么实现参考答案有好几种dispatch_group_tdispatch_group_notifydispatch_semaphore_t设置初始信号量为0每个请求完成时signal最后waitNSOperationQueue设置maxConcurrentOperationCount再设置依赖关系dispatch_group是最常用的。但笔试往往会给一个坑如果在子线程执行dispatch_group_wait会阻塞当前线程如果这个方法很不巧跑在主线程就会卡界面。所以更稳妥的做法是用dispatch_group_notify直接指定回调队列。另一个容易错的是信号量的signal和wait顺序。我见过有候选人把signal放在请求发起前这样最终结果根本不是等待N个请求而是直接通过了。记一个简单的原则signal必须放在任务完成后的回调里wait放在需要等待所有任务完成的那个线程上。4. 网络层与架构设计从答题到落地4.1 网络层设计思路URLSession 之上还有什么网络层的笔试通常不是让你写请求代码而是给你一个需求App需要统一处理登录态失效、统一参数签名、统一错误提示还要支持请求重试和取消你怎么设计网络层这是个开放题不用答成唯一标准但至少要体现分层思想。我的答案是底层用URLSession做真实网络传输但上层不要直接依赖它而是封装一个NetworkManager。NetworkManager负责把上层传进来的Request对象拼接成URLRequest加上公共参数、加密签名、用户token通过中间件依次处理最后发出去。Response统一解析把网络错误、业务错误、解析错误分开返回。强调一个点超时时间设置。URLSessionConfiguration里有一堆参数timeoutIntervalForRequest和timeoutIntervalForResource很多人混淆。前者是请求发起后等待返回的超时时间后者是资源加载的整体超时。在实际业务中上传接口的超时要明显大于普通GET请求不能用一个全局配置套所有接口。如果笔试里问到性能相关这个细节很加分。4.2 架构选型MVC/MVVM/组件化的判断标准架构题几乎是所有中大型公司笔试的常客。小满这套题里有一道“一个模块从MVC演进到MVVM你觉得解决了什么问题又引入了什么新问题”MVC的核心问题是Controller太重。UI事件、数据请求、模型转换、页面跳转都堆在一起代码超过一千行以后维护成本直线上升。MVVM把数据加工和业务校验放进ViewModel页面只负责绑定和渲染这样Controller的代码量会减少逻辑也更方便测试。但MVVM不是万能的。引入ViewModel后数据流会变复杂bind逻辑如果不规范照样会出现难以排查的问题。比如列表页的数据源是由多个请求合并得到的ViewModel内部必须理清请求顺序和失败重试逻辑否则页面很容易处于“缺数据”的状态。最好的答题方式是不站队说清楚什么场景下用MVC足够什么场景下值得上MVVM。组件化也是常见追问。答题时不要一上来就说“按业务拆模块”。组件化最重要的是回答“拆到什么粒度”和“模块之间怎么通信”。拆太细会导致工程文件爆炸拆太粗又起不到隔离作用。通信方案通常有Router注册、Block回调、NSNotification解耦具体选型要看业务是强依赖还是弱依赖。4.3 常见业务场景的架构取舍笔试里有一道场景题首页要同时展示多个业务模块的数据有的来自缓存有的来自网络有的来自推送预取你会怎么设计这个数据流这种题没有固定答案但候选人答得好不好差距很大。正确思路是先区分数据来源和更新时机再设计统一的ViewModel聚合层。每个子模块提供自己的数据Provider首页的ViewModel通过组合的方式把Provider的数据聚合起来分别监听变化再一次性刷新UI。能主动提“缓存策略”的候选人会占优势。比如内存缓存和磁盘缓存分别放什么内存缓存用什么淘汰策略磁盘缓存要不要做版本隔离。NSCache适合做内存缓存它可以在系统内存紧张时自动释放磁盘缓存则要考虑文件大小和过期时间。答题时把“缓存命中率”和“缓存新鲜度”这对矛盾提出来能体现你对业务的思考。5. 性能优化与Swift混合开发5.1 启动时间、卡顿、内存水位优化小满这批笔试的性能优化题比较接地气没有问底层汇编级的问题主要围绕启动速度和列表流畅度。启动时间优化的答题框架可以分三步度量、拆解、治理。首先用Instruments的App Launch模板拿到启动耗时拆成main函数前和main函数后。main前主要看动态库加载数量、load方法、static初始化。main后看AppDelegate里做了什么耗时操作。一个很常见的坑是很多初始化代码写在didFinishLaunchingWithOptions里而且是同步执行这会让首屏迟迟展示不出来。优化手段是延迟加载、放到子线程、用NSURLSession预加载数据。卡顿优化要围绕掉帧原理回答。屏幕每秒刷新60次每次刷新间隔16.7ms如果主线程在某个RunLoop周期内的任务超过这个时间就会出现掉帧。答题要点是区分CPU和GPU的耗时。CPU负责布局计算、图片解码、文本排版GPU负责合成渲染。常见的卡顿原因是主线程做了大量IO、复杂布局、离屏渲染。解决手段包括AsyncDisplayKit/Texture方案里的异步布局、CornerRadius合并、drawRect重写等。内存水位优化的关键是回答“怎么发现内存压力”和“怎么降低内存峰值”。发现层面可以用Xcode Memory Gauge和Instruments Allocations降低峰值可以从图片采样、autoreleasepool包裹循环、避免在循环内创建大量临时对象入手。5.2 Swift与OC混编的考察深度2023年的iOS笔试题Swift已经不局限于“会不会用”而是更看重“能不能理解混编工程”。混编的底层原理其实不复杂OC和Swift都依赖runtime但在工程里是两套编译单元。Swift文件暴露给OC需要生成-Swift.h头文件OC暴露给Swift需要Bridging-Header.h。笔试里考过“objc关键字什么时候必须加”。答案是需要暴露给OC调用的Swift方法或属性必须加objc如果还要被OC运行时动态派发可能还需要objc dynamic。另外Swift的泛型和OC的id之间不能直接对应OC里只能把Swift泛型类当作id来用这会导致类型信息丢失。如果混编项目里要传[String: Any]字典给OC通常要转成NSDictionary。类似这种“类型转换的坑”在笔试里只要踩准一两个点就能明显拉开和其他候选人的差距。5.3 系统能力边界与跨端思考还有一个让我印象深刻的题问的是“如果核心模块要用跨端方案iOS保留哪些部分”。这道题其实不是考技术选型而是考你对系统能力边界的判断。候选人如果只回答“用Flutter重写整个App”我觉得不是最优解。比较合理的回答是把UI层和轻量业务逻辑交给跨端框架但涉及系统级能力的地方保留原生实现。比如蓝牙、相机采集、推送token获取、账户安全模块这些对系统API依赖很强的地方放到原生否则跨端桥接会变成维护噩梦。答题时如果能说一句“每次桥接调用都有成本频繁跨线程/跨语言传大量数据也会成为性能瓶颈”会让批卷人觉得你有真实项目经验而不是只会看教程。6. 笔试中的常见问题与排查技巧实录6.1 候选人容易踩的答题坑结合我看了很多份答卷的经验踩坑主要集中在下面几类坑点具体表现改进方法概念回答过短只写结论不写推导补充一句话“为什么这样做”代码题不写异常分支只写happy path主动考虑失效场景开放性题没有结构想到哪写到哪用“思路-方案-取舍”三段式时间分配失衡前面题写太多后面空着先扫全卷后做代码题忽略笔试题干细节没发现需要合并两个请求圈出题目里的关键约束最可惜的一类情况是代码题整体思路是对的但没考虑线程安全问题。比如多个请求并发刷新同一个数据源如果没有保护会出现数据竞争。笔试中哪怕只是用一句话提示“这里需要加锁或用串行队列保证数据源安全”也会比单纯写业务逻辑更强。6.2 答题时如何控制节奏和深挖程度很多候选人担心笔试答得太浅就一直往外延伸结果适得其反。我的经验是一道题写到体系完整就够了不要无限扩展。什么算完整就是你提出了结论、给出了理由、补充了一两个边界条件然后就收住。比如内存管理题答到“ARC下编译器自动插入保留和释放因此不需要手动管理引用计数”已经能拿基础分再补充“但Core Foundation对象不归ARC管需要手动CFRelease”这就是加分项。加分之后就不用再往深挖了除非你确定有余力。如果遇到不会的题不要空着至少要写出你的初步推断。笔试题很多时候考的是分析能力不是唯一的正确答案。空着等于放弃写一个你认为可能的机制哪怕不完美也能让批卷人看到你思考的路径。我建议候选人平时刷题时按两套方式练习第一轮按知识点刷把OC、Swift、内存、网络、并发逐个击破第二轮直接做整卷模拟限定时间模拟真实笔试的节奏。第二轮比第一轮重要因为很多人在单点知识上有积累但缺乏组合运用和取舍判断的训练。最后说点个人体会做完这套笔试题我最大感触是iOS面试已经很少有那种“背一道是一道”的简单题了出题人越来越关心你能不能把知识点串成业务方案。比如内存管理、RunLoop、多线程单独看都是老生常谈但放在“网络请求并发回调刷新UI”的场景里就能看出你对整套机制的理解深度。如果你正在准备类似的笔试我建议把重点放在场景化练习上不要只盯着单一概念的结论。多做那种“给一段代码找出问题并修复”的题目多想想“为什么这样设计”。另外就是控制答题节奏模拟几套整卷别让分值最高的代码题毁在时间不够上。这套方法无论是对春招、秋招还是社招面试都很受用。
返回列表