iOS面试核心考点深度剖析:内存管理、多线程与Runtime

📅 发布时间:2026/9/1 14:39:56
iOS面试核心考点深度剖析:内存管理、多线程与Runtime 1. 写在前面这份题目为什么值得认真对待先说结论欢聚时代2018年校招iOS A卷放在今天来看依然是一份“含金量不低”的基础题。它不考花哨的Swift语法糖不考新框架API背诵而是把重点压在iOS开发最核心的几块地基上——内存管理、多线程、RunLoop、Runtime、UI布局、网络层。这些东西恰恰是很多工作了两三年的人依然说不透的内容。如果你正在准备iOS校招或者社招这份题目适合拿来当自测清单哪些题能脱口而出哪些题需要想一下哪些题完全没概念这就是你当前水平的真实剖面。我当年刷完这份卷子后的感受是学校教的那点东西和实际面试要的东西之间隔着一个“系统性整理”的距离。这篇文章我会按照“原题涉及哪些知识点→每类知识点的核心原理→我的答题思路和踩坑经验”这个逻辑来展开不贴原题原话毕竟涉及版权而是把题目背后考的每一个知识点拆开揉碎让你看完之后就算遇到变体题也能应对。2. 内存管理ARC时代为什么还要问MRC2.1 从一道“引用计数”题说开去A卷里内存管理的题目不少核心围绕引用计数、循环引用、weak和strong的区别、block对变量的捕获这几块。先说引用计数。很多新手会觉得“现在都ARC了谁还管retain和release”但面试官问这个不是为了让你手写release而是为了确认你是否真正理解对象的生命周期。引用计数的本质是一个整数每个OC对象内部都有一个retainCount当它为0时对象被释放。ARC做的事情就是在编译期帮你插入retain和release调用它不是一个运行时机制而是一个编译期机制。这一点很多解释都讲错了我在面试中反复被问到过所以单独拿出来强调。你要能说清楚strong对应retainweak对应assign但会在对象释放时自动置nilcopy对应retain加一次拷贝。在实际开发中weak最常见的应用场景就是delegate属性和block内部对self的弱引用前者是为了避免两个对象互相持有后者是为了打破block对self的强引用闭环。2.2 循环引用最常见的线上崩溃来源循环引用几乎是必考题。我给你一个标准的例子一个ViewController持有block属性block内部又使用了self。如果self对block是strongblock对self也是strong那两者就互相引用谁也释放不掉dealloc永远不会调用内存泄漏就产生了。解决办法三个思路第一种block内部用__weak修饰的self弱引用最常见也最推荐第二种在block执行完成后把block属性置nil强行打破闭环第三种如果block不需要长时间持有self可以考虑用__block修饰并在block内部置nil但这种方式在ARC下要小心因为__block对象的处理规则在不同版本里有变化。我踩过的一个坑是在block里调用了self的getter方法比如self.name然后编译器提示“Block implicitly retains self”。这说明即使用了weakSelf只要你在block里访问了self的某个属性编译器也会生成强引用。正确做法是在block外先定义__weak typeof(self) weakSelf self;然后在block内再定义一个__strong typeof(weakSelf) strongSelf weakSelf;用strongSelf来操作属性。这个“weak-strong dance”在SDK开发、网络回调里极其常见面试时能主动写出来是加分项。2.3 内存警告与ViewDidUnload2018年的题目里还有关于内存警告处理的题。对UIViewController来说系统在内存吃紧时会调用didReceiveMemoryWarning而对应的viewDidUnload在iOS 6之后就被废弃了不再调用。原因是系统不再默认释放self.view而是让开发者自己重写didReceiveMemoryWarning来释放可重建的资源比如图片缓存、大数组、动画等。很多老项目还留着viewDidUnload的代码在新系统上根本不会执行这就造成了一个假象开发者以为自己做了内存释放实际上什么都没发生。面试里提到这一点能体现你对系统版本差异的敏感度。3. 多线程与RunLoopiOS流畅度的底牌3.1 GCD的队列与死锁A卷里多线程的题目占了相当比重尤其是GCD。GCD的核心概念就两样队列和任务。队列分串行队列和并发队列任务分同步任务和异步任务。排列组合一下就有四种情况但这四种情况的执行时机和行为各不相同面试题经常从这里出。这里最经典的陷阱题就是“在主队列调用sync任务会发生什么”。答案是死锁。主队列是串行队列主线程上的任务必须一个接一个执行当你用dispatch_sync(dispatch_get_main_queue(), ^{})时相当于在主线程正在执行的任务中插入了一个“必须等这个block执行完才继续”的等待而主队列的机制决定这个block要等当前任务执行完才开始于是互相等待死锁就产生了。另一个高频考点是信号量dispatch_semaphore_t。它用来控制并发数的上限比如同时最多允许三个网络请求运行。这个在实际开发中很有用比如批量上传图片时限制并发数防止内存暴涨。3.2 RunLoop与界面卡顿的关联RunLoop这块面试官特别喜欢从“为什么滑动TableView的时候NSTimer不走了”这个问题切入。原因是RunLoop在滑动模式下会切换到UITrackingRunLoopMode而默认添加的Timer跑在NSDefaultRunLoopMode下滑动时默认模式下的任务不执行Timer自然就不触发。解决方案就是把Timer加到NSRunLoopCommonModes下这样Timer在Default和Tracking两种模式下都能跑。使用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]即可。更深一层RunLoop的source0、source1、timer三种事件源也是面试考点。source0是应用内部事件比如触摸事件source1是系统内核事件比如端口消息timer就是定时器。理解这些对排查界面卡顿很有帮助因为卡顿的本质就是主线程RunLoop无法及时处理事件perf工具抓到的通常就是主线程执行耗时操作时的堆栈。3.3 NSOperation与GCD的选择NSOperationQueue和GCD的区别也是个老生常谈的问题。我总结了一张对比表面试时可以直接用维度GCDNSOperationQueue底层C语言API轻量高效基于GCD封装OC对象取消任务不支持需自己写标志位原生支持cancel依赖关系需用dispatch_barrier或信号量模拟原生支持addDependency最大并发数信号量控制maxConcurrentOperationCount直接设置操作状态监听需手动管理KVO监听isFinished等结论就是简单一次性任务用GCD足够复杂任务编排、需要取消和依赖时用NSOperationQueue更合适。面试时把你的选择逻辑讲清楚面试官会觉得你是“带着方案思考”而不是“背概念”。4. Runtime与消息机制OC动态性的内核4.1 objc_msgSend的调用链Runtime几乎是每年必考2018年的A卷也不例外。核心考点就是objc_msgSend的执行流程消息发送时系统会沿着对象的isa找到其类对象在类对象的method list里查找方法找不到就去父类找最终到了NSObject还没有就进入消息转发流程。消息转发有三道防线动态方法解析resolveInstanceMethod、快速转发forwardingTargetForSelector、完整转发methodSignatureForSelector forwardInvocation。面试官问你“一个OC对象调用了一个不存在的方法会发生什么”你就应该把这个完整链路背出来。能答到forwardInvocation这一步就已经不错了。4.2 Method Swizzling的作用与风险Method Swizzling也是高频考点考察点在于“你会不会用”以及“你知道不知道它的风险”。它利用Runtime在运行时交换两个方法的IMP常用于AOP场景比如统计页面曝光、日志上传、埋点。常见的坑有三个第一交换方法应该在load中执行不要在initialize里反复执行第二交换时要注意被交换方法所在类的继承关系父类子类都要处理第三交换后不能调原方法的IMP否则会递归调用导致死循环。我自己就遇到过第三方SDK用Swizzling改变了UIViewController的viewWillAppear行为导致我们自己的统计代码重复上报的问题排查了很久才定位到是交换顺序冲突。4.3 KVC与KVO的实现原理KVC键值编码和KVO键值观察在2018年的题目中也有涉及。KVC的原理是当你调用setValue:forKey:时系统会先去查找setter方法setXxx找不到就用_xxx或xxx成员变量再找不到就调用setValue:forUndefinedKey:如果这个也没有实现就抛异常。KVO的实现原理更有意思当你对一个对象添加观察者时Runtime会动态生成一个子类NSKVONotifying_xxx重写被观察属性的setter方法在setter里同时调用willChangeValueForKey和didChangeValueForKey然后通知观察者。所以KVO只对KVC兼容的属性生效直接对成员变量赋值不会触发通知。5. UI与布局从Auto Layout到UITableView5.1 Auto Layout的优先级与固有内容大小A卷里有关UI布局的题主要集中在Auto Layout上。高频考点一个是intrinsicContentSize固有内容大小比如UILabel根据文字内容、字体大小能计算自己需要的尺寸UIButton则会根据图片和标题计算。理解了这一点你就知道为什么有时候约束明明写得很全UILabel还是撑不开或压缩重叠。另一个高频考点是约束的优先级required和optional。required优先级的约束必须全部满足optional约束可以冲突时自动放弃。这个在实际适配不同屏幕尺寸时很有用比如让按钮在“满足宽度约束的前提下尽量贴右”就可以把贴右约束设为optional。如果只是盲目把约束写满在小屏设备上就会出现约束冲突警告布局错乱。5.2 UITableView的复用机制与卡顿优化Cell复用是必考题但A卷考得更深一点不仅问“为什么复用”还问“怎么保证数据的正确性”。复用机制高效是因为避免了频繁创建和销毁UIView的昂贵开销。但复用的坑在于当你快速滑动时一个显示“已关注”状态的Cell滚出屏幕后又被复用到显示“未关注”的Cell上就会出现状态错乱。标准解法是在cellForRowAtIndexPath里把所有的状态按钮文案、图片、选中状态全部重新赋值不能依赖Cell之前的残留状态。另外iOS 5之后通过注册类和dequeueReusableCellWithIdentifier:forIndexPath:方式获取Cell时系统会自动创建并走初始化无需判空。关于卡顿优化核心是减少主线程的工作量Cell高度提前计算并缓存、图片异步加载、避免在drawRect里做复杂操作、把离屏渲染降到最低。A卷如果问“TableView滑动卡顿从哪些方面优化”这是一套完整的答法。5.3 iOS 11的Safe Area与适配2018年正好是iPhone X发布后的时间点A卷里出现了关于Safe Area的题目。iOS 11引入了SafeAreaLayoutGuide取代了之前的topLayoutGuide和bottomLayoutGuide。如果你的项目用Masonry这类框架要注意把约束绑定到safeArea上否则会出现刘海屏下布局被状态栏和底部横条遮挡的问题。一个比较实用的经验是在viewDidLayoutSubviews里通过self.view.safeAreaInsets获取安全区域的值做额外的适配逻辑比如判断底部横条高度来决定按钮的偏移量。这个值在viewDidLoad里拿不到要在viewDidAppear或viewDidLayoutSubviews里才有。6. 网络层从GET/POST到HTTPS与缓存策略6.1 HTTP协议基础与报文结构A卷的网络题不会让你贴一大段代码而是问原理性的内容。HTTP请求的本质是TCP连接上传输文本格式的报文报文由请求行、请求头、空行、请求体四部分组成。GET和POST的区别不只在语义上更实际的差异是GET参数拼在URL后面有长度限制POST参数放在body里容量大得多GET是幂等的POST不是幂等的。面试官经常会追问“POST能不能用URL传参”答案是“能”URL上可以拼任意参数服务端也未必拒绝但这不符合规范。我倾向于回答“可以但不建议”然后把原因讲清楚URL会出现在日志里敏感信息会泄漏URL有长度限制缓存和收藏夹会把参数带走。6.2 HTTPS握手过程与Charles抓包HTTPS的题目这些年越来越常见。核心是握手的简化流程客户端先发ClientHello带支持的加密套件列表服务端回ServerHello并附上证书客户端验证证书后生成预主密钥用服务端公钥加密发给服务端双方各自算出会话密钥之后用对称加密通信。理解这个过程对日常开发的帮助很大。比如你Charles抓包的时候SSL Proxying的原理就是Charles伪造了一个证书负责解密然后用你自己的证书和服务端通信负责加密相当于中间人的角色。所以在抓包时iOS端必须安装并信任Charles的根证书否则客户端会验证证书失败请求直接报错。我自己在这个环节踩过一个大坑App里用了证书锁定SSL Pinning也就是客户端内嵌了服务端的公钥或证书指纹此时Charles即使装了证书也解不了包因为客户端校验的是自己内置的那个证书。解决办法是debug模式下开放一个开关关闭证书校验或者在内置证书集合里临时加入Charles的证书。6.3 网络缓存策略与断点续传缓存策略也是常考点NSURLRequest的缓存策略有几种useProtocolCachePolicy默认、reloadIgnoringLocalCacheData忽略缓存、returnCacheDataElseLoad先用缓存没有就请求、returnCacheDataDontLoad只看缓存没有就失败。实际开发中我更习惯配合ETag和Last-Modified做服务端验证的缓存每次请求带上If-None-Match如果服务端资源没变就返回304客户端直接使用本地缓存。这样既保证内容新鲜又节约流量和带宽。断点续传涉及HTTP的Range头格式是Range: bytes0-1023服务端支持就会返回206状态码和对应分片数据。面试中如果能把这个细节答出来说明你真的做过下载功能。7. 常见陷阱与备考建议7.1 三张“问题速查表”我把这整套知识点整理成几个速查表方便你在面试前快速过一遍内存管理常见问题问题原因解决方案Block内使用self导致循环引用双方强引用互相持使用weakSelfstrongSelf定时器不释放NSTimer被RunLoop持有invalidate并置nil大图片内存暴涨解码后的位图占内存使用downsample或缩略图集合类持有大对象内存峰值过高使用autoreleasepool局部释放多线程常见陷阱问题原因解决方案主队列sync死锁串行队列互相等待改用async或子队列生产者消费者阻塞信号量使用不当注意wait/signal配对线程安全数据竞争多线程同时写可变数据加锁或用串行队列保护资源回收崩溃信号量或锁在dealloc中释放确保所有路径都释放UI布局常见异常问题原因解决方案约束冲突警告优先级设置不当调整约束priorityLabel内容截断固有大小计算失效设置preferredMaxLayoutWidthSafeArea遮挡未适配安全区域绑定safeAreaLayoutGuideCell状态错乱复用导致旧状态残留每次都重新赋值7.2 备考心得用“设计题”的方式学“概念题”2018年的A卷给我最大的启发是它考的不是API的“怎么用”而是API的“为什么”。单纯背结论应付不了追问必须真正理解底层原理才能面对变化多端的题目形式。一个建议是面对每个知识点都问自己三个“为什么”。为什么ARC能自动释放对象因为编译器插入了release调用。为什么block会捕获局部变量因为block本质上是一个结构体需要把变量值拷贝到结构体内部。为什么UILabel有固有大小因为它能根据内容计算需要的尺寸。当你把这三个为什么都能顺理成章地回答出来概念题基本就稳了。另外我强烈建议用“设计题”的方式来检验自己比如“让你设计一个图片加载框架你会怎么设计”你会自然想到内存缓存、磁盘缓存、异步下载、解码优化、取消机制、优先级调度而这些恰好覆盖了A卷里网络、多线程、内存管理三大主题。通过一道设计题把所有知识点串起来比一题一题刷效果要好得多。7.3 一个过来人的真心建议最后分享一个小经验。我记得当年做这套题的时候最让我难受的不是不会而是有些题“好像见过但说不清楚”。这种感觉比完全不会还要恼火。后来我意识到原因在于学习太碎片化今天看一篇博客讲RunLoop明天看一段视频讲Runtime知识没有形成体系到用的时候自然调不出来。所以我给你的建议是花一点时间把iOS的知识体系画成一张大图分成系统架构、语言特性、UI布局、网络、存储、多线程、性能优化这几块每一块再往下细分把自己会的内容标绿不会的标红然后用红色部分反推复习计划。这个过程很痛苦但坚持两周以后你面对面试题的心态会完全不同——你不会再有“考到不会的就完了”的恐慌因为你知道这个红色区域在哪个分支下你大概知道去哪里查、往哪个方向想。这份把控感比任何“面经”都值钱。