一、iOS的内存管理是啥
一说起内存管理,其实是一个很大的概念,但在任何系统中都有着重要的意义;这里只是基于参考资料和个人的理解大体框定一个概念
- 对于iOS来说,内存管理的核心是 "管控对象的生命周期",即确保 "有用的对象被持有、无用的对象及时释放";避免内存泄漏(无用对象未释放)和野指针(对象被提前释放后仍被访问)。其底层依赖 "引用计数" 机制,结合编译器(ARC)、运行时(runtime)、RunLoop 等协同工作,形成一套完整的内存管控体系。
- 谁持有,谁释放 ;无论 MRC(手动引用计数)还是 ARC(自动引用计数),内存管理的核心原则不变:
持有(Retain):当需要使用一个对象时,通过 "持有" 操作增加其引用计数,确保对象不被释放。
释放(Release):当不再使用对象时,通过 "释放" 操作减少其引用计数,当计数归 0 时,对象被销毁(调用 dealloc)。
禁止野指针:对象销毁后,需确保不再访问其指针(否则触发 EXC_BAD_ACCESS 崩溃)。
二、内存管理有啥东西
1. 引用计数(Reference Counting, RC)
引用计数是 iOS 内存管理的底层核心,本质是给每个对象分配一个 "计数器",记录当前持有该对象的 "引用者数量"。
a. 原理概述
- 计数增加(+1):表示有新的引用者持有对象,常见场景:
MRC 下调用 [obj retain]、[obj alloc](alloc 会默认将计数设为 1)、[obj copy](拷贝新对象,计数为 1)。
ARC 下用 __strong 指针赋值(如 self.obj = [NSObject new],编译器自动插入 retain)。 - 计数减少(-1):表示有引用者放弃持有对象,常见场景:
MRC 下调用 [obj release](计数减 1,若归 0 则销毁)、[obj autorelease](延迟减 1,由自动释放池处理)。
ARC 下 __strong 指针被置为 nil、超出作用域(如局部变量销毁),编译器自动插入 release。 - 对象销毁:当引用计数从 1 减至 0 时,系统会调用对象的 dealloc 方法(开发者可重写以释放资源,如移除监听、关闭文件);回收对象占用的内存(归还给系统,指针变为野指针)。
b. 底层实现 :
isa 与 SideTable 的协同
引用计数的存储分两种情况,取决于对象是否为 "非 tagged pointer"(如小整数、短字符串等特殊对象,直接存储在 isa 中,无引用计数):
- 64 位系统下的 isa 优化:isa 指针共 64 位,其中 extra_rc 字段(占 19 位)可直接存储引用计数(最多存储 2^19 - 1 = 524,287)。若计数未超过上限,直接存在 extra_rc 中,无需额外内存。
- SideTable 兜底:当引用计数超过 extra_rc 上限,或对象需支持弱引用(__weak)时,引用计数会存储在 SideTable 中。
SideTable 是一个全局哈希表,结构包含:
spinlock_t:自旋锁,保证线程安全(多线程操作引用计数时避免竞争);
RefcountMap:存储对象的引用计数(key 为对象指针,value 为计数);
weak_table_t:存储对象的弱引用列表(当对象销毁时,自动将所有弱引用置为 nil,避免野指针)。
c. 强引用 and 弱引用
引用计数的核心是 "强引用" 和 "弱引用" 的区分,这是避免循环引用的关键;
类型 | 关键字 | 对引用计数的影响 | 对象销毁后的行为 | 适用场景 |
---|---|---|---|---|
强引用 | __strong | 增加计数(持有) | 指针仍指向原地址(野指针) | 需长期持有对象(如属性、变量) |
弱引用 | __weak | 不影响计数(不持有) | 指针自动置为 nil | 避免循环引用(如代理、block) |
不安全弱引用 | __unsafe_unretained | 不影响计数(不持有) | 指针仍为野指针(危险) | 兼容旧版本,现已极少使用 |
默认规则:ARC 下,所有局部变量、属性(未指定关键字)默认是 __strong(强引用)。
2. ARC(Automatic Reference Counting)
ARC 是 iOS 5 引入的核心特性,本质是 "编译器自动插入引用计数操作(retain/release/autorelease)",开发者无需手动管理计数,大幅降低内存问题概率。
a. 原理概述(编译器和运行时协同)
-
编译期:编译器分析代码中对象的 "持有范围",在合适的位置自动插入 retain(如赋值给强引用时)、release(如强引用超出作用域时)、autorelease(如方法返回对象时)。
// 以一个简单的str举例:
NSString *str = [NSString stringWithFormat:@"test"];
// 编译后自动插入的代码(MRC 等价逻辑)
NSString *str = [[NSString stringWithFormat:@"test"] retain]; // 强引用持有,retain+1
// ... 使用 str
[str release]; // str 超出作用域,release-1 -
运行时 :runtime 辅助处理特殊场景,如:
- __weak 引用的自动置 nil(对象销毁时,runtime 遍历 SideTable 中的弱引用列表,将所有弱指针置空);
- autorelease 对象的延迟释放;
b. ARC下开发中需注意的点
(1)无需调用 retain/release/autorelease/dealloc 这些方法,否则编译会报错;
(2)使用@autoreleasepool 语法,而非 NSAutoreleasePool;
(3)不建议使用retainCount,因为ARC 会优化计数存储,retainCount获取的计数可能不准确;
(4)block 会自动捕获外部变量,若捕获 __strong 的 self,可能导致循环引用(需用 __weak 打破)。
3. 自动释放池(AutoreleasePool)
自动释放池是引用计数的 "补充机制",核心作用是 "延迟对象的释放时机",避免短时间内频繁创建 / 释放对象导致的性能开销;
- 批量释放:将多个 autorelease 对象集中在池销毁时统一释放,减少 release 调用次数;
- 主线程自动管理:由 RunLoop 自动创建 / 销毁池(进入时 push,休眠前 pop),开发者无需手动处理;
- 子线程手动管理:子线程无默认池,需手动用 @autoreleasepool 包裹任务,避免 autorelease 对象泄漏。
ps:整理了下有点多,这玩意以后单开一篇( ̄▽ ̄)~*;
4. RunLoop
RunLoop 虽不直接管理引用计数,但通过 "管控自动释放池的生命周期" 和 "调度任务执行时机" ,间接影响内存释放。有关RunLoop的详述见这里:iOS八股文之 RunLoop
这里简述其对内存管理的影响:
- 主线程自动释放池的 "管家":RunLoop 每次循环(处理 UI 事件、定时器等)都会创建新池,休眠前释放旧池,确保临时对象(如 UI 绘制产生的临时数据)及时释放;
- 避免主线程内存峰值:通过 "分批次释放",防止大量临时对象堆积在单次 RunLoop 循环中,导致内存骤升。
三、内存管理实践踩坑集
1. 循环引用:内存泄漏十个里面八个是它
循环引用是指 "两个或多个对象互相强引用",导致它们的引用计数永远无法归 0,最终内存泄漏。
举几个小🌰:
-
Block 捕获 self;Block 会强引用捕获的 self,若 self 又强引用 Block(如属性持有 Block),形成循环引用:
// 错误示例:self 强引用 block,block 强引用 self
self.myBlock = ^{
[self doSomething]; // block 捕获 self(强引用)
};
// 解决方案:用 __weak 修饰 self,让 Block 弱引用 self;
// 当然一些三方库的弱引用宏写起来更简洁,但注意多个库都有时的冲突;
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 避免 block 执行中 self 被销毁
if (strongSelf) {
[strongSelf doSomething];
}
}; -
代理(Delegate)用强引用;若代理属性用 strong 修饰,代理对象(如 VC)会强引用被代理对象(如 View),被代理对象又强引用代理,形成循环;
// 错误示例:代理用 strong
@interface CustomView : UIView
@property (nonatomic, strong) id<CustomViewDelegate> delegate; // 强引用代理
@end
// VC 中:self(VC)强引用 view,view 强引用 self(delegate)
self.customView.delegate = self;
// 解决方案:代理属性必须用 weak 修饰:
@property (nonatomic, weak) id<CustomViewDelegate> delegate; // 弱引用代理,打破循环 -
NSTimer 未及时释放;NSTimer 会强引用其 target(如 VC),若 VC 又强引用 NSTimer,且未在 VC 销毁前停止 Timer,会形成循环:
// 错误示例:VC 强引用 timer,timer 强引用 VC
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
解决方案:VC 销毁前停止并置空 Timer:- (void)dealloc {
[self.timer invalidate]; // 停止 Timer,解除对 self 的强引用
self.timer = nil; // 解除 VC 对 Timer 的强引用
}
- (void)dealloc {
2. 关于内存泄漏的多说几句:
- 常见的内存泄漏还有一些点如:
- 单例持有 VC / 大对象 单例生命周期与 App 一致,持有对象后永不释放;单例中避免持有 VC / 大对象,改用弱引用或临时持有
- 未移除通知监听 通知中心强引用监听者,未移除则监听者无法释放;在 dealloc 中调用removeObserver:self
- 大对象未及时释放 如 UIImage/NSData 占用大量内存,未置空;用完后立即将强引用置为 nil(ARC 下立即释放)
- WebView 未正确销毁 UIWebView/WKWebView 持有周期长,未释放;销毁前停止加载(stopLoading),置空代理;
- 排查工具:Instruments(核心工具)
- Leaks 模板:实时检测内存泄漏,标记泄漏对象的类型、创建栈,帮助定位泄漏代码;
- Allocations 模板:记录所有对象的内存分配情况,查看对象的 "存活数量""内存占用",分析是否有异常堆积;
- Memory Graph(内存快照):Xcode 内置功能(Debug → Memory Graph),可视化展示对象的引用关系,快速定位循环引用(如红色标记的循环引用链)。
3. 内存警告与处理
当 App 内存占用过高时,系统会发送 内存警告(Memory Warning),若不及时释放内存,App 会被杀死。处理流程如下:
- 接收内存警告的回调
UIViewController:- (void)didReceiveMemoryWarning;
AppDelegate:- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application; - 处理策略(核心:释放 "可重建" 的资源)
释放缓存:如图片缓存(SDImageCache 的 clearMemory)、网络请求缓存(NSURLCache 的 removeAllCachedResponses);
释放未显示的资源:如隐藏的视图、未播放的视频数据;
释放临时对象:如内存中的大列表数据(可重新从数据库 / 网络加载);
避免在警告回调中创建新对象(防止内存进一步升高)。
四、关于内存管理的其他补充
1. 不同对象类型的内存管理差异
- OC 对象(引用类型):依赖引用计数管理,ARC 自动处理;
- Swift 值类型(struct/enum):不依赖引用计数,存储在栈上(栈内存自动回收,无需管理),赋值时会拷贝(深拷贝);
- Swift 引用类型(class):与 OC 对象类似,ARC 管理,但 Swift 的 ARC 更严格(如引入 "所有权" 概念,避免跨模块的计数混乱);
- Tagged Pointer 对象:如 OC 的 NSNumber(小整数)、NSString(短字符串)、Swift 的 Int(小值),直接存储在 isa 指针中,无引用计数,销毁时无需释放内存(性能极高)。
2. iOS 版本对内存管理的优化
iOS 7+:UIWebView 优化内存占用,同时推出 WKWebView(内存管理更高效,推荐替代 UIWebView);
iOS 10+:系统支持 "内存压缩"(Memory Compression),当内存紧张时,系统会压缩部分内存页,而非直接杀死 App;
iOS 13+:UIScene 多窗口架构下,每个窗口的内存管理独立,某窗口销毁时其资源可单独释放。
3. 大内存对象的特殊处理
- 图片加载:避免直接加载原图(如 10MB 的图片),需按显示尺寸缩放(用 UIGraphicsImageRenderer 或 SDWebImage 自动缩放),减少内存占用;
- NSData 处理:大文件数据(如视频、压缩包)避免一次性读入内存,改用流(NSInputStream/NSOutputStream)分块处理;
- 缓存策略:大对象优先用磁盘缓存(如 NSCachesDirectory),而非内存缓存,内存缓存需设置上限(如 SDImageCache 的 maxMemoryCost)。
对于内存管理认识,应该渗透在整个iOS开发相关的各个细节里;理解透了内存管理,那也掌握了iOS的大部分;so,这里我肯定也写不全,越写越多,先到这( ̄▽ ̄)~*。