iOS技术专家高阶面试宝典 - 第3节

第3章 iOS内存管理与ARC机制:引用计数、循环引用、内存泄漏排查

本章聚焦面试中最高频的 iOS 内存管理考察点。你需要掌握 ARC 底层原理、循环引用的常见场景与修复方式、以及利用工具定位泄漏的实战能力。以下结构化内容将帮助你在面试中层层递进,展现专家深度。
内存管理答题框架 1. 理解ARC规则 2. 识别循环引用 3. 泄漏排查工具 4. 优化方案输出 alloc/retain/release autoreleasepool Block、NSTimer、Delegate __weak 打破强引用 Leaks / Allocations Malloc Debug / Analyze 修复 + 防回归单元测试

卡片1:ARC下引用计数工作机制

面试问题ARC 下引用计数是如何工作的?你能说说底层原理吗?
面试官考察点是否理解 ARC 是编译期自动插入 retain/release,还是运行期计数;是否清楚 __strong/__weak/__unsafe_unretained 的底层区别。
回答框架① ARC 编译器在适当位置自动插入 retain/release;② 引用计数为0时自动调用 dealloc;③ 注意 __weak 变量不增加引用计数,且对象释放后自动置 nil。
简短示例回答ARC 下编译器会在对象赋值、函数传参、返回值等时机自动插入 retain/release 调用,引用计数归零时触发 dealloc。例如 `self.obj = [[NSObject alloc] init];` 编译器会插入一次 retain。使用 __weak 修饰符可避免循环引用,但对象释放后弱引用自动变 nil。
可能追问① autoreleasepool 的作用时机? ② MRC 与 ARC 混编如何处理? ③ __unsafe_unretained 为什么不安全?
避坑提醒不要误以为 ARC 可以完全解决所有内存问题——循环引用仍需手动处理;不要在 dealloc 中调用 `[self release]` 或 `[super dealloc]`,ARC 下禁止。

卡片2:循环引用的场景与解决方案

面试问题请列举 iOS 中常见的循环引用场景,并说明如何解决?
面试官考察点能否快速定位 Block、NSTimer、Delegate、通知等典型场景;是否掌握 __weak、__block、手动释放等不同解决方案的优劣。
回答框架① 场景:Block 捕获 self → 使用 __weak(typeof(self)) weakSelf;② 场景:NSTimer 强引用 target → 在合适时机 invalidate;③ 场景:Delegate 双方强引用 → 一方用 weak 修饰。
简短示例回答Block 中如写成 `self.property = ^(id obj) { [self doSomething]; }` 会形成 retain cycle。解决方案:`__weak typeof(self) weakSelf = self;` 后在 block 内使用 weakSelf。对于 NSTimer,除 weak 外还必须在 dealloc 或 viewDidDisappear 中调用 [timer invalidate] 并置 nil。
可能追问① 为什么 __block 不能解决 Block 循环引用? ② NSTimer 在什么情况下即使使用了 weakSelf 仍会泄漏? ③ Swift 中闭包循环引用的处理方式与 OC 有何不同?
避坑提醒避免滥用 __block 来持有 self,在 MRC 下 __block 不持有,但 ARC 下 __block 会强引用,反而加重循环。Swift 中必须使用 `[weak self]` 或 `[unowned self]`。

卡片3:内存泄漏排查流程与工具实战

面试问题你如何排查 iOS 应用中的内存泄漏?请描述完整的排查流程及常用工具。
面试官考察点是否具备成熟的调试经验:能否使用 Instruments 定位到具体代码行;是否了解静态分析、Malloc Debug 等高级技巧;是否能在项目早期预防泄漏。
回答框架① 静态分析:Xcode → Product → Analyze;② 动态分析:Instruments 的 Leaks + Allocations;③ 精确定位:Malloc Stack 记录分配栈;④ 修复与验证:修完后用 Leaks 重新跑一遍。
简短示例回答打开 Leaks 工具运行 App,点击功能模块后观察红色泄漏点。双击泄漏对象查看引用树,发现某个 AVPlayer 没有释放。再用 Allocations 记录其引用计数历史,定位到在编辑器中未移除 KVO 导致。修复后在 dealloc 中 removeObserver 并释放,重新检测无泄漏。
可能追问① 如何区分内存泄漏与内存暴增? ② Instruments 的 Mark Generation 功能如何使用? ③ 如果泄漏对象是系统类(如 _NSMutableDictionary)如何反查业务代码?
避坑提醒不要只依赖 Leaks 工具,有时泄漏不会立即触发(如 Timer 未释放的延迟泄漏),应结合 Allocations 的持久化对比。Use Malloc Stack 时注意开启“所有分配”模式,否则部分系统内存不会被记录。

项目案例:短视频社交App“闪音”内存优化

项目名称闪音 — 短视频拍摄与上传社区
业务背景用户使用手机拍摄 15~60 秒短视频,进行滤镜/特效编辑后上传。App 需同时处理相机实时预览、多段视频拼接、内存占用极高。
规模指标日活用户 50 万,视频编辑模块内存峰值从 480 MB 优化至 220 MB,崩溃率从 0.3% 降至 0.02%。
技术栈Swift + UIKit + AVFoundation + Kingfisher(图片缓存) + Alamofire + GPUImage3
核心难点AVPlayer 在编辑器中被多处持有未释放,导致页面 pop 后播放器仍存活;滤镜链中 CIFilter 对象累积;视频合成时内存一次性暴增。
面试高频追问1. 你如何定位到 AVPlayer 泄漏的具体代码位置?
2. 优化方案中是否考虑过复用 CIFilter 以减少创建?
3. 如何保证优化后不引入新的竞态条件?
可迁移回答话术“我使用 Instruments 的 Leaks 工具运行编辑流程,发现 AVPlayer 实例未释放。接着用 Allocations 启用 Malloc Stack 记录所有分配,查看泄漏对象的引用树,定位到在 EditorViewController 中创建了 AVPlayer 并添加了时间观察者(addPeriodicTimeObserver),但 dealloc 中仅 removeObserver 却未将 player 置 nil。修复方案:在 dealloc 中调用 player.pause()、removeObserver、并设置 player = nil。类似方案也可迁移至 GIF 播放器、语音播放器等场景。”

“对于 CIFilter 累积问题,我采用享元模式——在编辑器生命周期内复用相同类型的滤镜实例,并在退出编辑时批量清理。减少约 35% 的内存抖动。”

“为确保线程安全,优化中所有对 AVPlayer 的操作在主线程串行执行,并通过 weakSelf 避免闭包循环引用;新增单元测试确保 dealloc 后对象引用计数归零。”
专家提示:面试中描述内存优化项目时,务必突出“工具定位 → 根因分析 → 方案设计 → 指标验证”的完整闭环。将每一次泄漏抽象为通用模式(如观察者未移除、Timer 未释放),展示你的工程化思维。