恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
2023小满秋招iOS笔试复盘:底层原理与实战坑点解析
首页
资讯中心
/
2023小满秋招iOS笔试复盘:底层原理与实战坑点解析
2023小满秋招iOS笔试复盘:底层原理与实战坑点解析
发布时间:2026/9/1 7:25:31
1. 笔试整体感悟与核心考点分布2023年度小满秋招iOS研发岗第一批笔试我是在截止日前两天突击完成的。说实话现在很多大中厂的iOS岗位笔试已经不再满足于“你背了多少API”而是更看重语言底层理解、内存管理机制、多线程与网络层面的综合工程能力。这套题目给我的整体感觉是广度覆盖正常深度偏重底层且带了不少“实战踩坑题”和市面上纯背八股文的套路有明显的区分度。整套题大致可以分成几个模块Objective-C与Swift语言基础、内存管理、Runloop与多线程、网络与性能优化、UI布局与渲染、架构设计、以及最后的代码题。从难度梯次上看前两个模块属于送分题中段属于区分题末尾属于压轴筛选。如果你准备过一段时间iOS面试及格并不难但想拿到高分进入下一轮需要具备真实的项目沉淀尤其是对崩溃排查、内存优化、线程安全问题有感性认识。另外要提醒一点小满的系统是牛客网在线笔试摄像头监控全程不允许切屏。虽然iOS开发本身不强制要求Mac环境但如果你平时用Windows考前一定要提前熟悉在线IDE的补全策略——它不会像Xcode那么智能很多代码题需要手写完整关键字这直接影响答题速度。2. 核心必考点拆解与出题逻辑2.1 语言基础Objective-C和Swift各占半壁江山第一部分主要是选择题考点集中在分类、关联对象、属性关键字、Swift值类型与引用类型、泛型、协议关联类型等。我印象比较深的一道题是在Objective-C中分类Category能否直接添加实例变量答案是不能。分类的底层结构体category_t中只有实例方法列表、类方法列表、协议列表和属性列表没有ivars。很多人会混淆“属性”和“实例变量”实际上通过property在分类中声明属性仅仅会生成setter/getter声明并不会自动生成带下划线的成员变量和对应的实现。想要在分类里“存东西”通常需要关联对象Associated Object。如果你在项目中动态给UIView绑定一个事件回调用的就是objc_setAssociatedObject它和分类属性是两个层面的东西。Swift部分则考了闭包捕获列表、逃逸闭包、weak与unowned的选择原则。这类题看起来简单但最容易丢分的是unowned的安全性问题——一旦被捕获对象提前释放访问unowned会直接触发野指针崩溃而weak只是变成nil。苹果官方文档的表述是“使用unowned时你必须自行保证对象的生命周期”但很多实际项目中为了安全都会优先weak。语言基础模块我觉得背后的出题逻辑很清晰不考语法糖考你踩没踩过坑。分类能不能加属性这类题刷再多LeetCode也练不出来只有真在项目里拆过组件、写过公共库的开发者才会理解。2.2 内存管理引用计数、weak实现与循环引用这一块是iOS笔试的大头。小满的题目出了一道关于weak属性在运行时如何被置为nil的细节题选项包括“通过运行时消息发送”、“通过哈希表遍历”、“通过释放对象时回调”正确答案是基于SideTable 中的弱引用表weak_table_t来找到所有指向该对象的弱引用指针逐一置为nil。我可以稍微展开讲一下底层流程对面试也很有帮助对象释放时会走dealloc-objc_destructInstance-clearDeallocating。clearDeallocating内部会从全局的SideTable中取出对应的弱引用表。弱引用表中以对象指针为key存储所有指向该对象的弱引用地址weak_entry_t。遍历这些地址将它们的值统一设为nil。这就是为什么__weak修饰的变量在对象释放后读取值为nil而不是野指针。循环引用问题更是常客。题目给了三个场景让你选出不会造成循环引用的组合Block内使用self且self持有该BlockNSTimer被self持有timer的block又强引用selfDelegate属性用weak修饰第三项是安全的。而NSTimer这里即便你用了__weak只要self持有timer、timer又持有self依然会形成循环。解决办法是在viewWillDisappear或dealloc中主动invalidate。但注意只要timer还在被RunLoop持有dealloc就不会被调用也就没有机会在dealloc里invalidate所以正确的做法是用一个中间对象做代理或者改用iOS 10的block版timer并在合适的时机释放。我平时在项目里更推荐一套组合方案用NSProxy做一次转发切断timer对self的直接持有同时在页面viewDidDisappear时执行invalidate。这样既能保证定时器正常触发又不会因为返回上级页面导致永久循环。2.3 RunloopSource0/Source1与自动释放池关于Runloop小满考了一道比较有区分度的题哪些事件会导致RunLoop唤醒标准回答应该是这三类Source1基于端口的系统事件比如CFMachPort、CFMessagePort由内核驱动Source0需要手动CFRunLoopSourceSignal并CFRunLoopWakeUp才能触发的事件Timer定时事件到了系统通过mach_msg唤醒 RunLoop还有一个隐藏考点是Observer回调比如kCFRunLoopBeforeWaiting与kCFRunLoopAfterWaiting它本身不唤醒线程但在状态切换时被系统调用。另外一道让我印象深刻的题是为什么主线程的RunLoop默认是开启的而子线程默认不开启主线程的RunLoop在UIApplicationMain启动时自动创建并运行它负责处理触摸事件、CADisplayLink、NSURLSession回调、CATransaction提交等而子线程的RunLoop需要你手动调用[[NSRunLoop currentRunLoop] run]才会创建。值得注意的是子线程RunLoop默认没有注册任何Input Source和Timer所以直接run会立刻返回线程也就随之退出。正确做法是给RunLoop添加一个NSMachPort或者一个持续性的Timer才能让RunLoop保持运行。这块题目还顺带问到了autoreleasepool的释放时机。实际上主线程RunLoop在每次循环即将进入睡眠时kCFRunLoopBeforeWaiting会自动销毁并重建一次自动释放池。所以你在子线程中批量创建大量临时对象时一定要自己手动加autoreleasepool否则这些对象会一直累积到子线程RunLoop的空闲时机才释放严重的内存峰值就是这么来的。3. 多线程、网络与稳定性题型3.1 多线程与锁死锁现场分析多线程模块小满出了一道经典的dispatch_sync死锁题在主线程执行dispatch_sync(dispatch_get_main_queue(), ^{})会发生什么为什么发生死锁。原因是dispatch_sync会阻塞当前线程等待block执行完毕但block又提交到主队列主队列的任务调度依赖主线程执行而主线程现在被dispatch_sync阻塞住了彼此等待形成死锁。类似地如果在串行队列的某个block内部再次dispatch_sync到同一个串行队列也会死锁。而dispatch_async不会因为它不阻塞当前线程只会把block追加到队列尾部等待执行。这道题本身不难但很多人会忽略一个变体场景通过dispatch_semaphore等待主线程任务完成但等待的代码本身又在主线程执行同样会卡死主线程。我就在一次启动优化中踩过这个坑本意是想在启动时同步等待某个异步配置加载完成结果主线程被信号量卡住异步任务根本没机会回调整个启动流程直接卡在那一行最后用超时机制和回调重写才解决。关于锁的知识题目中还考察了不同锁的对比。我把常用的几种锁总结成一张表锁类型使用方式性能适用场景synchronized最方便自动加解锁最慢低频操作、保护代码块NSLock显式加锁/解锁中等一般互斥场景pthread_mutexC语言API较快需要跨平台或底层控制os_unfair_lock替代OSSpinLock很快短临界区dispatch_semaphore信号量控制很快生产者-消费者、资源池atomic属性修饰符较快单属性读写但无法保证多语句原子性这里额外提醒一个常见误区atomic并不是线程安全的代名词。它保证的是getter/setter本身的原子性但如果你执行“读取并修改”这种复合操作仍然需要加锁。比如self.count 1即使count声明为atomic也不是安全的因为本质上是读、加、写三个操作。3.2 网络层HTTP与HTTPS握手细节网络类题目里小满考了HTTP/2和HTTP/3的对比还有TCP与UDP的应用场景区分。HTTP/2的多路复用解决了队头阻塞问题但它基于TCP依然存在TCP层面的队头阻塞HTTP/3改用QUIC基于UDP实现可靠传输从根上解决了这个问题。关于HTTPS笔试中出了一道流程排序题要求把握手过程按顺序排列。标准流程是客户端发起ClientHello包含支持的TLS版本、加密套件列表、随机数服务端回复ServerHello选定加密套件和TLS版本服务端下发证书链Certificate服务端发送ServerHelloDone客户端验证证书生成预主密钥用服务端公钥加密后发送ClientKeyExchange双方基于预主密钥和随机数生成会话密钥客户端发送ChangeCipherSpec后续改用对称加密通信前些年还会出现一个考点是关于客户端是否校验证书链的。实际开发中如果服务端使用了自签名证书你要在URLSession:didReceiveChallenge:回调里做处理但千万不要在正式环境全局信任自签名证书否则中间人攻击直接暴露你的数据。另外iOS开发中还常考DNS解析与缓存策略。题里问到了NSURLCache的默认内存和磁盘容量以及URLRequest.CachePolicy的几种模式。这个知识点看起来不起眼但在弱网环境下一个合理设置的缓存策略可能决定你的App是秒开还是白屏三秒。3.3 稳定性崩溃类型与防护机制稳定性题目比如“如何防止因NSDictionary插入nil导致崩溃”标准做法是统一封装一层安全字典或者使用YYKit或JRLDictionary这样的方案重写setObject: forKey:判断nil与NSNull。另外NSArray越界崩溃、NSMutableArray在遍历过程中删除元素崩溃也都是高频题。如果你做过崩溃平台监控一定知道常见的崩溃类型分Objective-C异常NSException、EXC_BAD_ACCESS野指针、SIGABRT断言失败、watchdog超时等。笔试中问了一个实际场景线上崩溃日志显示SIGSEGV发生在objc_msgSend你会如何排查首先要看崩溃堆栈中是否有类名和方法名有的话大概率是对象被提前释放可以在Address SanitizerASan和Zombie Objects环境下复现如果没有类名可能是内存被整体破坏比如数组越界写。这类题考的不是某一个API而是你的整体排查思路。4. UI布局与渲染优化重点4.1 Auto Layout与UIStackViewUI部分的题目集中在Auto Layout、UIStackView、UITableView的复用机制和渲染优化上。有一道选择题问在UIStackView中如何让一个子视图保持固定宽高正确做法是给该子视图添加widthConstraint和heightConstraint而不是在StackView上直接设置distribution为fillEqually。fillEqually会让所有子视图等宽如果要保持某一个固定尺寸需要显式约束。另外UIStackView在iOS 11之后加入了嵌套支持可以用嵌套UIStackView构建复杂布局但要注意嵌套层数过深会降低布局性能。Auto Layout相关的另一道题是setNeedsLayout、layoutIfNeeded和layoutSubviews的区别setNeedsLayout打个标记告诉系统当前视图布局已失效等下一个刷新周期再重新布局不会立即触发。layoutIfNeeded立即检查是否需要重新布局如果需要就马上调用layoutSubviews。layoutSubviews真正执行布局逻辑的地方通常不手动直接调用。平时做动画的时候经典写法是先setNeedsLayout然后在动画block里调用layoutIfNeeded这样系统会在约束变化后平滑地过渡到新布局而不是瞬间跳变。4.2 渲染流程与离屏渲染关于渲染流程题目问到iOS中Core Animation提交的时机是什么什么时候会触发离屏渲染Core Animation在RunLoop即将进入休眠或被事件唤醒时会将CATransaction提交给渲染服务。主线程RunLoop的Observer监听kCFRunLoopBeforeWaiting和kCFRunLoopAfterWaiting两个时机所以你在一个循环里连续修改五个View的frame最终也只会有一次渲染。离屏渲染则是很多App卡顿的元凶。常见的触发场景有添加cornerRadius且masksToBounds YES设置shadowPath之前给layer添加阴影同时未指定路径shouldRasterize YES使用UIVisualEffectView模糊效果其中圆角的处理是有争议的。如果只是一张小图片加圆角并不会带来明显性能问题但如果在UITableView滚动时对大量cell添加圆角离屏渲染的代价会立刻显现。我的建议是能提前切好圆角图的就用图片不能的话用UIBezierPath做遮罩再不行才考虑cornerRadius。4.3 性能优化列表流畅度与启动时间列表优化是所有iOS性能优化的重头戏。小满这道题给出了一个场景列表滚动掉帧你会如何定位和优化我的排查路径一般是这样先用Time Profiler抓主线程耗时看是CPU瓶颈还是GPU瓶颈。如果在cellForRowAtIndexPath中做了同步网络请求或大量文本计算把耗时操作移到子线程。用Instruments里的Core Animation工具看Color Hits Green and Misses Red排查离屏渲染和shouldRasterize的使用是否合理。如果是图片解码耗时不使用UIImage直接加载大图而是用WebP或DownsamplingCGImageSourceCreateThumbnailAtIndex先压缩到目标尺寸。最后用os_signpost做分段打点把各环节的时间分布量化再做针对性优化。启动时间优化这块笔试中出了一个开放性问题如何将冷启动时间降低20%可以从几个方向入手pre-main阶段的动态库瘦身与load方法清理、main阶段的任务延迟与按需初始化、首屏阶段的异步渲染。其中load是一个很容易被忽视的优化点很多三方SDK喜欢在load里做method swizzling这会拖慢启动。如果可能尽量将其迁移到initialize或首次调用时。5. Block、架构设计与组件化实践5.1 Block的底层原理与循环引用Block是iOS面试里绕不开的重点笔试中的考察方式也更贴近实际。第一道题Block外部变量捕获规则。对于局部变量Block会按值捕获除非用__block修饰对于静态局部变量Block按引用捕获对于全局变量Block直接访问无需捕获第二道题__block的作用是什么底层如何实现__block会使外部变量被包装成一个__Block_byref_xxx结构体Block内部通过该结构体修改真实值所以能改变外部变量。但注意在ARC环境下__block修饰的对象变量会被强引用而__weak修饰的不会。第三道题Block的存储位置。_NSConcreteGlobalBlock定义在全局作用域、且不捕获外部变量的Block存储在数据段_NSConcreteStackBlock捕获了外部变量的Block最初存储在栈上_NSConcreteMallocBlock在ARC下栈上Block被copy到堆上后存储位置为堆ARC下编译器会自动帮我们做较多copy操作所以StackBlock在ARC程序中很少手动出现但在MRC或某些C混编环境下你必须手动调用Block_copy否则Block在函数返回后就不可用了。5.2 架构设计MVVM分层与依赖注入架构题部分小满给出了一个中大型App的模块拆分场景要求选择合适的分层方案并解释理由。我用的是MVVM 服务层分层的思路。MVVM相比MVC的优势在于将ViewController中大量的业务逻辑抽取到ViewModel层ViewController只负责视图绑定与用户事件转发。但MVVM也有过度设计风险——如果你只是做一个工具型的小AppMVC反而更直观、维护成本更低。面试时我会强调架构是为团队协作、测试便利和需求迭代服务的不是为简历服务的。组件化方向题目问到了模块间如何通信。目前主流方案有基于CTMediator的目标-动作方案基于路由表的URL路由方案基于协议和依赖注入的方案我的实际项目用的是协议化方案每个组件暴露一组接口协议由统一的管理类注册和查询实现。优势是编译期就能发现实现类缺失调用方不依赖具体类名天然支持单元测试Mock劣势是需要维护协议定义和实现类的注册关系初期工作量略大。5.3 组件化拆分与二进制化组件化拆分的第一步是搞清楚边界。我会把App拆成三层基础层网络、存储、日志、埋点、中间层账号、支付、分享、Push、业务层Feed、消息、我的。每一层之间只通过协议通信严禁跨层直接依赖实现类。在开发阶段组件以源码方式集成方便调试在打包阶段我倾向于将稳定组件做二进制化.xcframework显著降低编译时间。但二进制化也有坑比如不同Xcode版本编译出来的产物兼容性、Bitcode的开启与否、以及LLVM版本不一致导致的链接问题。如果你团队不大编译时间还在可接受范围内建议先不要盲目二进制化把这笔账算清楚再说。6. 代码题与现场实操复盘6.1 算法题LRU缓存设计代码题部分小满出了一道经典题实现一个LRU缓存要求get和put的时间复杂度均为O(1)。我的做法是用哈希表 双向链表class LRUCache: def __init__(self, capacity: int): self.capacity capacity self.hashmap {} self.head Node(0, 0) self.tail Node(0, 0) self.head.next self.tail self.tail.prev self.head def get(self, key: int) - int: if key in self.hashmap: node self.hashmap[key] self._remove(node) self._add(node) return node.value return -1 def put(self, key: int, value: int) - None: if key in self.hashmap: self._remove(self.hashmap[key]) node Node(key, value) self.hashmap[key] node self._add(node) if len(self.hashmap) self.capacity: lru self.head.next self._remove(lru) del self.hashmap[lru.key]源码里我单独定义了Node类包含prev和next指针。哈希表负责O(1)定位节点双向链表负责O(1)的删头和尾插。笔试环境里没有Xcode我直接用Python手写因为核心考察的是数据结构和逻辑不要求具体语言。这种题最容易出错的地方是删除最久未使用节点时忘记同步更新哈希表或者新插入节点时没有处理容量为0的边界情况。我在牛客上提交前特意检查了这两个细节。6.2 设计题短链系统设计另一道设计题是设计一个短链系统支持生成短链并支持跳转要求估算容量和存储方案。我是这样分析的容量估算假设每天新增100万条短链每条记录约100字节一年存储约36GB完全可以放在MySQL中如果数据量再大可以考虑分库分表或最终归档到冷存储。生成策略用发号器Redis INCR或数据库自增ID生成唯一ID再通过Base62编码转换为短链。短链长度6位即可容纳约620亿种组合足够长期使用。重定向浏览器请求短链时服务端返回302Location指向原长链。302相比301的好处是方便统计点击量和修改目标地址不会被浏览器永久缓存。缓存热点短链可以缓存到Redis预期命中率超过80%可以显著降低数据库压力。这段设计思路比较朴素但胜在每一步都讲清楚了“为什么这样做”而不是只会堆概念。6.3 实战复盘内存泄漏排查最后一道代码分析题给我一段已有内存泄漏的代码要求指出问题并修复。核心场景是一个单例对象持有了一个blockblock内部又持有了另一个对象导致该对象永远无法释放。修复思路是在合适的时机将block置空或者将单例对block的属性改为weak或者在block内部使用weak-self打破持有链。此外我还补充了一点如果你用的是NSTimer还需要注意RunLoop对timer的强引用即使你手动置空了属性timer也可能还在事件循环中运行。排查这类问题推荐两个工具Leaks检测内存泄漏对象Allocations查看对象的创建与释放记录。如果是线上问题可以结合Malloc Stack记录所有内存分配栈再配合Xcode的Memory Graph Debugger做嫌疑对象分析。7. 简历与软技能考察要点7.1 项目经历的表述方式笔试完成后还有一轮简历筛选性质的问答实际是在线填写项目经历。小满给出的模板是“项目背景、个人职责、遇到的技术难点、解决思路、最终成果”五个维度。这里想给大家一个建议不要用“参与了xx模块开发”这种流水账表述而是每句话都量化。比如不要说“优化了启动速度”要说“通过调整动态库加载顺序、清理load方法、将非首屏任务延迟到didFinishLaunching之后将冷启动时间从860ms降低至670ms”。面试官看简历的时间有限数据永远比形容词有说服力。7.2 团队协作与主动性在线笔试中还有一组场景题考察团队协作能力。比如如果需求方的需求与你的技术判断产生冲突你会怎么处理常规思路是先理解对方的业务背景再说明技术方案的利弊如果依然无法说服对方可以借助数据和小范围实验来验证。注意这类题的隐藏评分点是“你是否能站在业务角度理解技术选型”而不是单纯坚持“我的技术是对的”。比如一个看起来短期需要频繁变更的需求你用高复杂度方案去实现虽然架构上更优雅但可能并不符合团队当前阶段的最优解。8. 应试策略与后期复盘经验整套笔试做完我的直接感受是如果你在平时项目里没有真正排查过线上崩溃、没有做过启动优化、没有处理过线程竞争很多题即使见过答案也未必能答得稳。这和刷LeetCode不太一样iOS笔试更考验经验沉淀的深度而不是短时间的突击能力。备考阶段我整理了三条比较实用的策略。第一把语言基础题背到条件反射。分类和关联对象、Block捕获规则、weak原理这些题属于“送分题”不允许丢分第二用真实项目反向推导知识点比如你最近刚修过一个循环引用崩溃就把Runloop、Block、dealloc、Instrument这整条链路的所有考点都复习一遍这样记忆最牢固第三考前限时模拟。在线笔试的节奏比想象中紧代码题不止一道如果平时习惯Xcode的自动补全手动编写时会很不适应最好提前用网页IDE练习。最后分享一个我个人的体会笔试真正筛掉的不是基础差的同学而是“只会背答案但不会分析问题”的同学。答题过程中每一道题都可以看作是面试官在问你“你有没有在实际开发中真的遇到过这个情况”你需要在答案中体现出场景、原因、手段和结果而不只是一个孤立的知识点。扎实理解、踏实复盘才是通过iOS秋招笔试最快的路。