ARTICLE DETAIL

资讯详情

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

iOS内存管理详解:从引用计数到循环引用,面试与实战全攻略

iOS内存管理详解:从引用计数到循环引用,面试与实战全攻略 1. 为什么“iOS内存管理”是面试必考题也是实战分水岭做iOS开发这几年我面试过不少人也被面试过不少次。只要岗位要求里写着“扎实的计算机基础”内存管理几乎是逃不掉的一关。它不像UIKit那样背几个API就能糊弄过去而是真正能区分“会用Xcode写界面”和“理解系统运行机制”的试金石。先给个结论iOS内存管理的核心就是引用计数Reference Counting。无论是早期的MRC手动引用计数还是现在大家天天用的ARC自动引用计数底层都是围绕“谁持有对象、谁负责释放”这套逻辑在转。Swift虽然听起来换了套语言但本质依然是引用计数那一套只是编译器帮你把retain、release插进了代码里。这套机制能解决的问题非常直接移动端内存有限一个App如果无限创建对象而不释放很快就会被系统杀掉。而内存管理做得好不好直接决定了App在复杂页面、图片加载、网络请求并发场景下是流畅还是卡顿闪退。这篇文章适合三类人准备iOS面试的求职者想系统梳理内存管理知识树的初中级开发以及被线上闪退和内存暴涨折磨到想放弃的运维同学。我会从底层原理讲到高频面试题再给出一套能直接用的排查工具链和避坑清单争取让你看完就能在面试里把这块讲得比面试官还熟。2. 内存管理的基础模型从“谁污染谁治理”说起2.1 引用计数到底在计算什么先忘掉所有iOS术语把内存里的对象想象成一间共享办公室。谁要用这间办公室谁就先登记名字走的时候再把自己的名字划掉。只有当登记表上没有任何名字时行政才会把这间办公室清空、收回。这里的“名字”就是引用计数每多一个人使用计数加1释放一个人计数减1归零即销毁。Objective-C的对象管理就是这个模型。早期MRC时代开发者需要手动调用retain加1、release减1还要小心autorelease这种“延迟释放”的操作。一旦多写了release对象提前销毁再访问就是野指针崩溃少写了release对象永远不销毁内存泄漏。ARC出现后编译器在编译期替你做retain和release的插入工作。你只需要用strong、weak这些所有权修饰符声明“我打算怎么持有这个对象”剩下的交给编译器。但ARC不是垃圾回收它没有后台线程去扫描哪些对象没用了它只是“编译时自动插入内存管理代码”的静态分析加代码生成机制这一点在面试时一定要说清楚。2.2 MRC时代的经典代码长什么样先看一段MRC时代的代码能帮助你理解今天ARC帮你省了什么事// MRC 手动内存管理 - (void)configureWithModel:(Model *)model { // 先持有传入的对象防止它在方法执行期间被释放 _model [model retain]; } - (void)dealloc { [_model release]; _model nil; [super dealloc]; }看上去不复杂但一旦涉及多个对象相互引用、数组存取、block回调捕获手动管理就成了灾难现场。我在面试的时候偶尔会让候选人手写一段MRC下的setter方法实现- (void)setName:(NSString *)newName { if (_name ! newName) { [_name release]; _name [newName retain]; } }为什么要先release旧值再retain新值因为如果先retain新值再release旧值当新旧是同一个对象时会多一次release导致过度释放。这种细节就是实战经验和纯背题的差距。2.3 ARC到底是“自动”在哪一层ARC的全称是Automatic Reference Counting自动引用计数。它做的事情是在编译期分析对象生命周期在你的代码里自动插入retain、release、autorelease调用。ARC下你写这段代码NSString *str [[NSString alloc] initWithFormat:%, hello];编译后等价于NSString *str [[NSString alloc] initWithFormat:%, hello]; // 编译器自动知道 str 持有这个对象不需要 retain // 当 str 超出作用域时编译器自动插入 [str release]ARC的规则有几个重点不能手动调用retain/release/autorelease/dealloc只能重写dealloc做资源清理strong和weak决定了持有策略unsafe_unretained和assign不参与持有管理对象释放后指针不会自动置空存在野指针风险。3. iOS内存布局与Objective-C对象结构从底层理解内存去哪了3.1 可执行文件里的五大内存区域一个iOS App跑起来后内存空间大致分成几块区域代码区、常量区、全局区静态区、堆区、栈区。理解这个分布对分析内存问题非常关键。代码区存放编译后的机器指令常量区存放字符串常量等不可变数据全局区存放全局变量和静态变量栈区存放局部变量、函数参数由系统自动分配和释放空间小但速度快堆区存放通过alloc、new、copy等动态创建的对象需要我们管理生命周期空间大但分配速度比栈慢。常见误区是把“栈上分配的局部变量”和“堆上分配的对象”混为一谈。比如- (void)foo { NSObject *obj [[NSObject alloc] init]; }obj这个指针变量本身在栈上但[[NSObject alloc] init]创建的对象在堆上。obj只是指向堆区对象的栈上指针。当方法结束栈上的指针变量会被回收但堆上的对象是否释放取决于引用计数是否归零。ARC会在obj离开作用域时自动插入release让堆区对象的引用计数减1。3.2 isa指针与Tagged Pointer的优化Objective-C对象的本质是结构体指针每个对象内存布局的第一项就是isa指针指向它的类对象。在64位系统下Apple对isa做了优化它不再单纯存储类地址而是一个联合体部分位存类信息部分位存引用计数、是否弱引用等额外信息。还有一个重要的优化叫Tagged Pointer。对于NSNumber、NSDate这类小对象如果值本身很小系统不会在堆上分配内存而是直接把值编码进指针里。这样就不需要引用计数管理访问速度也大幅提升。你在面试中如果聊到“为什么NSNumber比较快”能提到Tagged Pointer是个很大的加分项。关于对象内存对齐所有Objective-C对象的内存大小都按8字节对齐。你可以用class_getInstanceSize查看一个类的实例占多少内存用malloc_size查看系统实际分配了多少内存。前者往往是后者去掉16字节头部后的值这16字节就是isa和一些内存管理信息。4. 循环引用与weak底层面试连环问的核心区域4.1 循环引用的三种经典场景循环引用是iOS内存管理面试里最高频的考点基本属于必考。它的定义很简单两个对象互相强引用导致彼此的引用计数永远不为0系统无法释放它们。第一个经典场景是delegate。很多初学者容易把delegate声明成strong这就会造成循环引用// 错误示例delegate 用 strong修饰 property (nonatomic, strong) idMyDelegate delegate; // 正确做法delegate 用 weak 修饰 property (nonatomic, weak) idMyDelegate delegate;为什么delegate要用weak因为通常持有关系是A创建BA成为B的delegate。如果B也strong持有delegate那么A持有BB持有A谁也跑不掉。weak修饰后B只是“借用”一下delegate的执行能力不实际持有它。第二个经典场景是Block。Block在捕获外部变量时如果捕获的是一个对象并且这个对象也持有这个block就形成循环引用self.myBlock ^{ [self doSomething]; // self 强持有 blockblock 捕获 self循环 };解决方案是使用weakSelf。iOS 12之后Xcode 10的新编译器支持了__weak __typeof(self) weakSelf self;的简写__weak typeof(self) weakSelf self; self.myBlock ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } };这里建议在block内部先转成strongSelf避免在block执行到一半时weakSelf被释放导致异常。这也是面试追问点为什么weakSelf转strongSelf不会造成循环引用因为strongSelf是block内部的局部变量它的强引用在block执行结束后就释放了不会持续持有self。第三个经典场景是NSTimer。NSTimer会强持有target如果timer又被self持有就形成循环// self 强持有 timertimer 强持有 target(self) self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:selector(tick) userInfo:nil repeats:YES];iOS 10之后可以用block方式创建timer内部实现不会强持有target但要注意block也有可能捕获self还是要用weakSelf。另外NSTimer一定要在合适的时机invalidate否则不光循环引用还可能导致页面已经离开但timer还在跑频繁回调造成资源浪费。4.2 weak表的实现原理聊到weak面试官大概率会问weak变量为什么在对象释放后自动变成nil这背后的机制是runtime维护了一张全局的weak表SideTable里的weak_table。大致流程是这样当你用__weak修饰一个变量并指向对象时runtime会把这个weak指针地址注册到对象的weak表中。当对象的引用计数归零、准备销毁时runtime会遍历weak表中所有指向这个对象的弱指针把它们全部置为nil然后再执行dealloc。这张weak表其实是一个哈希表key是对象地址value是一个数组存着所有指向该对象的weak指针地址。想深入的话可以直接读runtime源码里的objc-weak.mm文件面试时能讲出“SideTable管理、weak_entry_t结构、dealloc时清空”这几个关键词基本就能证明你真正读过源码。4.3 strong、weak、copy、assign到底怎么选属性修饰符是iOS面试的基础题几乎每个候选人都会被问到。我的建议是先记住一条原则对象类型默认用strong委托/代理用weak字符串和Block用copy基本数据类型用assign。strong强持有引用计数加1对象生命周期内一直存在。weak弱持有引用计数不加1对象释放后指针自动置nil。copy复制一份新对象常用于NSString、NSArray、NSDictionary这些有可变/不可变之分的类型。用copy可以防止外部修改了可变字符串导致内部属性也被改动。assign通常用于基本数据类型如NSInteger、CGFloat不涉及引用计数。如果错误地用于对象类型对象释放后指针不会置nil就会野指针崩溃。unsafe_unretained跟assign类似不持有对象但对象释放后指针不置nil容易野指针除非有特殊需要否则不要用。关于copy有一个容易踩的坑如果用copy修饰NSMutableArray调用addObject时直接崩溃因为copy回来的是不可变NSArray。以前项目里就遇到过同事把NSMutableArray声明成copy结果一运行就闪退排查半天才发现是这个问题。4.4 autoreleasepool与RunLoop的关系自动释放池是另一个高频考点。autoreleasepool的作用是延迟对象的释放相当于把它扔进一个“待释放清单”等这个池子被销毁的时候统一release。iOS App启动后系统在主线程RunLoop里会自动创建autoreleasepool每个RunLoop周期结束时会自动drain一次。这也是为什么你在主线程写代码很少需要手动创建autoreleasepool的原因。需要手动创建autoreleasepool的场景主要有几种大量循环创建临时对象时主动包一层来及时释放内存自己创建后台线程时线程没有系统的autoreleasepool需要手动包一层在一些高内存压力的工具类方法里也适合手动包一层降低峰值内存。一个面试里常见的坑是下面这行代码会不会泄漏- (void)foo { for (NSInteger i 0; i 100000; i) { autoreleasepool { // 创建大量临时对象 } } }答案是如果不包autoreleasepool这100000个临时对象会全部进入当前RunLoop周期的自动释放池循环结束后才一次性释放峰值内存可能很高。包了autoreleasepool后每个循环体内生成的对象在循环结束时就被释放内存曲线平稳得多。5. 内存优化实战从Instruments定位到代码层面的针对性优化5.1 用Instruments的Leaks和Allocations定位问题工具永远是排查内存问题最快的手段。在Xcode菜单栏选择Product - Profile打开Instruments选择Leaks模板就能实时看到内存泄漏点。Allocations模板能看到整个App各类型对象的分配情况和内存占用曲线。我用Leaks模板排查泄漏的通用步骤是这样点击录制按钮在App里复现可疑操作路径比如反复进出某个详情页。观察Leaks面板是否出现红色泄漏条。注意Leaks需要一定时间跑才能汇总检查有时刚出现但现在还不准建议多等几秒。选中泄漏条目点击Call Tree栏折叠系统库调用只显示我们自己App的符号能快速定位到是哪个方法、哪一行创建的对象没有被释放。跳转到对应的代码位置检查有没有循环引用、block捕获self、NSTimer未invalidate等问题。Allocations模板里有个很实用的功能Mark Generation。进入一个页面前点一次Mark离开页面后再点一次Mark然后对比两次之间的内存变化。如果反复进出导致内存一直涨不回落基本可以断定有对象泄漏了。这个方法比Leaks更敏感因为Leaks只能检测无人持有的对象如果对象被互相持有但整个子图已经不可达也能检测出但如果对象被泄漏的全局缓存持有Allocations的Mark Generation对比更好用。5.2 图片加载与内存峰值的几个实战技巧图片是iOS内存占用的大头。一张1920x1080的JPEG约几百KB解压成位图后在内存里是按像素计算的宽度x高度x4字节大约8MB。一个页面加载10张图就是80MBApp不卡才怪。实际项目里我常用这几个优化手段使用[UIImage imageWithContentsOfFile:]替代[UIImage imageNamed:]加载一次性大图后者有系统缓存用完不释放。加载大图时预览压缩到目标尺寸再渲染。可以用CGImageSourceCreateThumbnailAtIndex直接生成缩略图避免完整解压大图。对列表中的图片使用SDWebImage这类带缓存和异步加载的第三方库或者自研类似机制避免重复加载和解压。利用CADisplayLink或滚动代理做懒加载只渲染可视区域内的图片。这里多说一句很多人以为imageNamed:有缓存就很好用但它容易造成内存中堆积大量不再展示的图片。适合用imageNamed:的场景是App图标、TabBar图标这类频繁使用且体积小的图。5.3 定位内存泄漏MLeaksFinder与Leaks的不同视角MLeaksFinder是腾讯开源的内存泄漏检测工具它的检测思路跟Instruments完全不一样。它的核心原理是使用一个对象后在延迟一段时间后检查该对象是否存活。如果存活就判定为泄漏并自动弹出提示。它能检测到ViewController、View、Block的循环引用引起的泄漏。接入非常简单在Podfile里加pod MLeaksFinderDebug模式下跑起来就行。它对项目中常见的“push进A页面再pop出来A页面没有被释放”这种问题能非常直观地弹窗告诉你哪个对象泄漏了。但它也有误报情况比如缓存对象、单例、NSTimer目标、被其他工具类持有较长时间的对象可能会误判。我遇到最多的是子线程里使用了UI相关对象或者延时操作导致延迟释放MLeaksFinder延迟判断时对象还没释放完就弹窗了。所以在使用时要设置白名单过滤掉正常的常驻对象。5.4 内存警告处理与didReceiveMemoryWarning在内存紧张时系统会向App发送内存警告。对iOS开发者来说处理内存警告也是基本功。AppDelegate和UIViewController都可以收到didReceiveMemoryWarning回调实际项目里常用的处理方式清空图片缓存、数据缓存比如SDWebImage的[[SDImageCache sharedImageCache] clearMemory]。移除当前不在界面上的大图对象让它们可以重新加载。及时停止正在执行的耗时操作释放计算资源。对于可重建的资源对象置为nil等待后续重建。网上有个经典操作在didReceiveMemoryWarning里把ViewController的view置空。但这会的副作用是如果你没有正确处理view的重建逻辑返回页面时会白屏新手慎用。6. 面试高频题速查与答题套路整理6.1 50个高频问答速查我整理了面试中被问到的内存管理类问题能覆盖80%以上的考察面问题答题核心要点高赞加分项ARC和MRC的区别是什么ARC是编译期插入retain/release不是垃圾回收补充说明ARC对代码块的优化autoreleasepool的工作机制RunLoop周期结束时drain延迟释放补充手动创建autoreleasepool的场景block什么时候会循环引用block被self持有且捕获self时强调weakSelf转strongSelf的原因NSTimer的循环引用怎么解决iOS10后建议block方式及时invalidate补充RunLoop Mode对timer的影响为什么delegate要用weak避免双向强持有补充delegate在MRC下的处理copy和strong有什么区别copy会复制新对象防止外部修改补充可变/不可变类型对比什么对象会被放到自动释放池中非alloc创建的对象常进池补充autorelease和alloc的对比如何检测循环引用Instruments Leaks、MLeaksFinder、代码审查现场手动画出引用链有没有遇到过内存泄漏的坑讲述一次真实项目经历最佳给出排查流程和解决前后对比如何降低内存峰值懒加载、缩略图、autoreleasepool、缓存清理补充使用Instruments排查峰值的方法深复制和浅复制浅复制只复制指针深复制复制对象内容补充iOS对象实现NSCopying协议的要求_unsafe_unretained和weak区别unsafe_unretained不自动置nilweak自动置nil补充unsafe_unretained在dealloc里的坑表格里没有提到的还有iOS内存地址空间的分配规则、什么是内存对齐、Tagged Pointer是怎么回事这些你按照前面章节我讲的答一遍就足够。6.2 面试官的追问套路怎么应对面试官不会满足于你背出一个正确答案他更关心的是你有没有真正理解背后的原理。举个例子当你答“delegate要用weak防止循环引用”面试官可能追问weak和assign有什么区别如果你答不上来weak基于SideTable实现、对象销毁时自动置nil而assign不会就会显得知识深度不够。再比如聊到autoreleasepool他可能追问ARC下什么时候需要手动创建autoreleasepool如果你只答“不知道怎么用”基本就凉了。实际上你只要说出大量循环创建临时对象、后台线程这些场景再讲一下为什么这些场景需要手动管理pool就能把主动权拿回来。我觉得应对追问的最好方法是顺着“为什么”这条线多想两层。面试官问的是“是什么”你就答完“是什么”后主动说出“为什么是这样”和“不这样会怎样”。这不仅是在展示知识量更是在展示思维方式。7. 几款常见的误用场景与避坑心得7.1 开发中常见的内存误用先说几个我在代码Review时最常看到的误用。第一种无视Core Foundation层面的对象。比如使用CGImageRef、CGColorSpaceRef这类C层面的对象需要手动调用CGImageRelease、CGColorSpaceRelease释放。很多人用CGBitmapContextCreate创建上下文后不释放每生成一张图就涨几十MB内存。第二种滥用单例存储大对象。单例一旦创建就不会释放如果你把大量数据、图片、ViewModel放进单例属性里基本等同于永久泄漏。我曾经在一个项目里看到同事把当前用户信息全部存进单例包括头像图片切换账号时只清用户名不清图片导致内存一直稳态上涨。第三种在block里使劲捕获self。现在Xcode会有内存图分析工具提示但仍然看到很多代码直接在block里操作self一点警惕心都没有。记住一个口诀block被self持有block里就要weakSelf。如果你不确定block是否被self持有用Xcode的Debug Memory Graph跑一遍最稳妥。7.2 一次线上内存暴涨的排查复盘有一次线上反馈说App运行几分钟后变得奇卡内存从正常100MB涨到500MB。我用Instruments的Allocations抓了一遍发现增长全是_NSContiguousString。再往调用栈看是某个大字符串拼接操作。代码大概是这样的NSString *result ; for (NSString *item in array) { result [result stringByAppendingString:item]; }在循环里每次拼接都会生成新的字符串对象旧对象要在antoreleasepool drain后才释放。数组大时峰值内存会爆炸。修复方案很简单NSMutableString *result [NSMutableString string]; for (NSString *item in array) { [result appendString:item]; }用可变字符串在循环里原地追加内存占用平稳得多。这个案例我在面试里讲过很多次一方面体现实际排障能力另一方面也说明内存管理不只存在于ARC和block里单纯写代码的方式就能造成巨大内存差异。7.3 关于内存泄漏检测工具的选择工具没有银弹我的建议是搭配使用Xcode自带的Debug Memory Graph适合手动点击界面时观察对象是否存在尤其是检查被弹出的ViewController有没有释放。Instruments Leaks适合整体扫描确认是否有泄漏。MLeaksFinder适合开发阶段接入自动弹窗提醒能够尽早发现。线上环境的Retain Cycle检测可以接入一些APM工具做低成本的实时监控。我的个人习惯是开发阶段开着MLeaksFinder每次UI操作后如果弹窗就即时排查。上架前用Instruments的Leaks跑一遍关键路径回归。线上则用监控大盘看内存增长率超过阈值自动报警。三条线同时进行基本能把内存问题控制在可控范围。8. 给准备面试的同学最后一点建议如果你最近在准备iOS面试我建议把内存管理的复习顺序这样安排先弄懂引用计数模型和ARC的底层本质再掌握Objective-C对象的内存布局和isa的概念然后重点突破循环引用的三大场景和weak的实现原理最后把Instruments和MLeaksFinder用熟。这个顺序是层层递进的面试官问你问题的时候你能从底层原理一直讲到实际排查工具他想不给你高分都难。我最近在面试中也发现一个现象很多人能脱口而出“delegate要用weak、block要weakSelf”但问他为什么、底层怎么实现的、遇到循环引用怎么定位就支支吾吾说不出来。这就是典型的“背题不背书”问题。建议每个点都自己动手验证一遍写个循环引用的demo用Instruments看到泄漏再修好代码再看到曲线下降。这个过程比背十道面试题都管用。还有一个小技巧面试时可以用Xcode的Memory Graph手动演示一遍push一个页面、pop出来、看navigationController的viewControllers数组、再用Debug Memory Graph选中该页面对象看引用链。如果你能现场演示出循环引用的完整链路面试官基本就没什么可追问的了。最后想说的是内存管理不只是为了面试它直接关系到你写的App在用户设备上是否流畅、是否会被系统杀掉。理解了这套机制你在处理Autolayout卡顿、图片加载慢、列表滚动掉帧这些问题时都会有新的思路。希望这篇文章能帮你在面试中多拿几分更希望它能在你实际写代码时帮你避开那些曾经让我踩过的坑。
返回列表