ObjC内存管理
# 04.ObjC内存管理
# 目录介绍
- 4.1 生命周期设计
- 4.2 内存管理核心机制
- 4.3 手动引用计数 (MRC)
- 4.4 自动引用计数 (ARC)
- 4.5 内存最佳实践
- 4.6 循环引用排查与实战
- 4.7 ARC 下的常见坑
- 4.8 实战案例:内存泄漏排查
- 4.9 autorelease 机制深入
# 4.1 生命周期设计
对象的生命周期是指对象从创建到销毁的整个过程。了解对象的生命周期对于正确管理内存和资源以及避免潜在的问题非常重要。
# 4.1.1 初始化操作
1.分配(Allocation):在分配阶段,通过使用alloc和init方法或其他创建对象的方法,为对象分配内存空间,并初始化对象的实例变量。
MyObject *obj = [[MyObject alloc] init];
- alloc:为对象分配内存。
- init:初始化对象,设置默认值或执行其他初始化操作。
2.初始化(Initialization):在初始化阶段,可以在init方法中对对象的实例变量进行初始化,设置初始状态和执行其他必要的操作。
- (instancetype)init {
self = [super init];
if (self) {
// 初始化实例变量
}
return self;
}
// 任何类在实例化之前都会先调用该方法
+ (void)initialize {
//这个比init方法先调用
}
+initialize vs +load:
+initialize | +load | |
|---|---|---|
| 调用时机 | 第一次接收到消息时(懒加载) | 应用启动时立即调用 |
| 调用方 | objc_msgSend(正常消息发送) | 直接函数指针调用 |
| 子类继承 | 如果子类未实现,会调用父类的 | 不继承,每个类独立 |
| 典型用途 | 类级别初始化(如注册默认值) | Method Swizzling |
# 4.1.2 销毁时释放
1.释放(Deallocation):在释放阶段,对象不再被使用,内存空间将被释放。当对象不再被引用时,Objective-C的自动引用计数(ARC)机制会自动处理对象的释放。
obj = nil; // 对象将被释放
2.释放前的清理(Cleanup before Deallocation):在对象被释放之前,可以在dealloc方法中执行一些清理操作,如释放资源、取消观察者、解除通知等。
// 对应于 initialize 方法,用于清空对象,当引用计数为 0 时被调用
- (void)dealloc {
// 如果未启用 ARC,则需要手动释放引用计数
[nickName release];
// 清理操作
[super dealloc];
}
对象销毁的完整流程(release 导致引用计数归零时):
release → refcount == 0
→ objc_destructInstance(obj)
→ 调用 .cxx_destruct(C++ 析构)
→ 调用 associated objects 清理
→ 清空 weak 引用(置 nil)
→ free(obj) ← 归还内存
# 4.2 内存管理核心机制
# 4.2.1 引用计数原理
引用计数是 Objective-C 内存管理的核心机制。每个对象都有一个引用计数器,用于记录当前有多少个对象在使用它。
- 当对象被创建时,引用计数为 1。
- 当对象被其他对象引用时,引用计数加 1。
- 当对象不再被引用时,引用计数减 1。
- 当引用计数为 0 时,对象被销毁,内存被释放。
引用计数的四个基本操作:
| 操作 | MRC 方法 | ARC 等价 | 效果 |
|---|---|---|---|
| 创建 | alloc / new / copy / mutableCopy | 同左 | refcount = 1 |
| 持有 | retain | strong 赋值 | refcount + 1 |
| 释放 | release | 离开作用域 | refcount - 1 |
| 延迟释放 | autorelease | @autoreleasepool | 稍后 release |
# 4.2.2 引用计数存储方式
引用计数不存储在对象本身——而是存在一个全局的 "Side Table" 中(现代 Runtime):
对象 (obj) 全局 Side Tables
┌──────────┐ ┌──────────────────┐
│ isa │ │ Table[index] │
│ 成员变量 │ ───────→ │ ├─ weak_table │ ← 存储所有 weak 指针
└──────────┘ │ ├─ refcnts[0] │ ← 引用计数 (2 位)
│ ├─ refcnts[1] │ ... 64 位 bitmap
│ └─ ... │
└──────────────────┘
isa 指针的低位/高位存储了一个 "side table index",
通过此索引找到全局 Hash 表中的对应条目。
为什么这么设计? 避免每个对象都携带引用计数字段(对象大多是 1-2 次引用,无需额外存储);集中管理的 Hash 表可以批量操作、更容易实现弱引用表。
isa 指针的多重身份(64 位 ARM):
从旧到新的演进:
旧式: isa = 类对象指针
新式: isa = 位域 (bitfield)
bit 0 : nonpointer (是否位域)
bit 1 : has_assoc
bit 2 : has_cxx_dtor
bits 3-27 : shiftcls (类指针,通过移位还原)
bits 28-47: extra_rc (额外的引用计数)
bit 48-63 : side table index
# 4.2.3 内存管理方式概览
内存管理是一项重要的任务,用于管理对象的生命周期和内存分配。在Objective-C中,有两种主要的内存管理方式:
手动引用计数(Manual Reference Counting,MRC)和自动引用计数(Automatic Reference Counting,ARC)。
MRC vs ARC 一句话总结:MRC 是"手算账单"——每次借还都要记账;ARC 是"会计代管"——编译时自动插入 retain/release 代码。
# 4.3 手动引用计数 (MRC)
# 4.3.1 MRC案例
手动引用计数(MRC):在MRC中,开发者需要手动管理对象的引用计数,通过调用retain、release和autorelease等方法来增加或减少对象的引用计数。需要确保在不再使用对象时,适时地释放其内存。
MyObject *obj = [[MyObject alloc] init]; // 引用计数为1
[obj retain]; // 增加引用计数
//释放对象
[obj release]; // 减少引用计数
[obj autorelease]; // 在自动释放池中延迟释放
# 4.3.2 MRC所有权规则
手动管理引用计数的方法
retain:增加对象的引用计数。release:减少对象的引用计数。autorelease:将对象添加到自动释放池,稍后释放。dealloc:对象被销毁时调用的方法,用于释放资源。
// 创建对象,引用计数为 1
NSObject *obj = [[NSObject alloc] init];
// 增加引用计数
[obj retain]; // 引用计数为 2
// 减少引用计数
[obj release]; // 引用计数为 1
// 将对象添加到自动释放池
[obj autorelease]; // 引用计数为 1,稍后释放
// 手动释放对象
[obj release]; // 引用计数为 0,对象被销毁
MRC 的四条黄金法则(谁创建谁释放):
| 方法前缀 | 所有权 | 需要 release? |
|---|---|---|
alloc / new | 调用者拥有 | ✅ 必须 |
copy / mutableCopy | 调用者拥有 | ✅ 必须 |
retain | 调用者拥有 | ✅ 必须 |
其他方法(stringWithFormat: 等) | 不拥有(已 autorelease) | ❌ 不需要 |
# 4.3.3 使用自动释放池
自动释放池用于延迟对象的释放。当对象被发送 autorelease 消息时,它会被添加到当前的自动释放池中。当自动释放池被释放时,池中的所有对象都会收到 release 消息。
@autoreleasepool {
// 创建对象并添加到自动释放池
NSString *str = [[[NSString alloc] initWithFormat:@"Hello, %@", @"World"] autorelease];
// 使用对象
NSLog(@"%@", str);
// 自动释放池结束时,str 会被释放
}
@autoreleasepool {} 的编译器展开:
@autoreleasepool { → void *ctx = objc_autoreleasePoolPush();
// 代码 // 期间 [obj autorelease] 把 obj 加入此池
} → objc_autoreleasePoolPop(ctx);
// 对池中所有对象调 release
为什么需要 autorelease
- 一个方法创建了对象并返回给调用者 —— 不能在自己内部 release(否则返回悬空指针),也不能 retain 后不释放(泄漏)
- 解决方案:autorelease —— 把释放责任交给"外层的自动释放池",保证调用者在当前 RunLoop 迭代期间能安全使用
# 4.4 自动引用计数 (ARC)
ARC 是 Objective-C 的编译器特性,它自动管理对象的引用计数,开发者无需手动调用 retain、release 和 autorelease。
# 4.4.1 ARC案例
自动引用计数(ARC):在ARC中,编译器会自动插入适当的引用计数管理代码,开发者无需手动管理对象的引用计数。编译器会根据代码的上下文自动添加retain、release和autorelease等操作。
MyObject *obj = [[MyObject alloc] init]; // 引用计数为1
obj = nil; // 引用计数 = 0,对象被销毁
// 不需要手动调用retain/release/autorelease
ARC 的核心承诺:只要你写对了"谁引用谁",编译器保证"该 release 时一定 release"。
# 4.4.2 ARC编译器原理
ARC 编译器用静态分析决定在哪里插入 retain/release。核心规则:
赋值(=)时:
strong 目标 ← 新值 → [新值 retain]; [旧值 release];
weak 目标 ← 新值 → 不 retain
离开作用域时:
strong 局部变量 → [obj release];
函数返回值时:
返回对象 → [obj autorelease];(在调用者的释放池中释放)
ARC 下源码 vs 编译器生成的等价 MRC 代码:
// 开发者写的 ARC 代码:
- (void)loadUser {
NSString *name = [self fetchName]; // 返回的 autorelease 对象
self.userLabel.text = name; // strong → retain
// 离开作用域 → release name
}
// 编译器生成的等价 MRC 代码:
- (void)loadUser {
NSString *name = [self fetchName]; // [obj retain] / autoreleased
NSString *oldLabel = _userLabel;
_userLabel = [name retain]; // setter 内 retain+release
[oldLabel release];
[name release]; // 离开作用域
}
# 4.4.3 属性修饰符全景
| 修饰符 | 引用计数操作 | 对象释放后 | 用途 |
|---|---|---|---|
strong | [new retain] + [old release] | 引用计数 -1 | 持有(默认) |
weak | 不操作计数 | 自动置 nil | 委托、避免循环引用 |
copy | [new copy] + [old release] | 同 strong | 不可变对象(NSString 等) |
assign | 不操作计数 | 悬空指针 | 基本数据类型 |
unsafe_unretained | 不操作计数 | 悬空指针(危险) | 极少数兼容场景 |
1.强引用(Strong Reference):默认情况下,对象之间的引用是强引用,即强制保持对对象的引用,只有当所有强引用都被释放时,对象才会被释放。
// 默认值,变量会被保存在内存中直到离开作用域
__strong NSString *strongString;
2.弱引用(Weak Reference):使用__weak修饰符可以创建弱引用,弱引用不会增加对象的引用计数,当对象被释放时,弱引用会自动设置为nil。
// 弱引用,假如引用的对象被释放,那么弱引用会被设为 nil
__weak MyObject *weakObj = obj;
// 与弱引用类似,但是如果引用的对象被释放,引用也不会被设为 nil
__unsafe_unretained NSArray *unsafeArray;
3.循环引用(Retain Cycle):循环引用指的是两个或多个对象之间相互持有强引用,导致它们无法被释放。为了避免循环引用,可以使用__weak修饰符、__unsafe_unretained修饰符或使用弱引用的代理对象等方式。
__weak typeof(self) weakSelf = self;
# 4.4.4 weak 变量的实现原理
weak 是所有修饰符中最复杂的——它需要运行时支持:
持有 weak 引用时:
1. objc_storeWeak(&weakPtr, obj)
2. Runtime 将 weakPtr 的地址加入 obj 对应的 Side Table 中的 weak_table
3. 不增加 obj 的引用计数
对象被销毁时(dealloc):
1. objc_destructInstance 遍历该对象的 weak_table
2. 将所有 weak 指针指向的地址 → 写入 nil
3. 清理 weak_table 中已处理的条目
weak vs unsafe_unretained 的核心区别——weak 在对象释放后自动置 nil,访问 nil 不崩溃;unsafe_unretained 留下悬空指针,访问即崩溃。永远优先用 weak。
# 4.5 内存最佳实践
# 4.5.1 避免循环引用
循环引用会导致内存泄漏。使用 weak 引用或 __weak 修饰符来打破循环引用。
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 使用 weakSelf 避免循环引用
};
# 4.5.2 管理临时对象
在循环中创建大量临时对象时,使用 @autoreleasepool 及时释放内存。
for (int i = 0; i < 1000; i++) {
@autoreleasepool {
NSString *tempStr = [NSString stringWithFormat:@"Temp %d", i];
NSLog(@"%@", tempStr);
}
}
# 4.5.3 dealloc 清理清单
在 dealloc 方法中释放对象持有的资源。
- (void)dealloc {
// 释放资源
[_resource release];
[super dealloc];
}
ARC 下 dealloc 的标准清理清单:
- (void)dealloc {
// 1. 移除 KVO 观察者
[self.person removeObserver:self forKeyPath:@"age"];
// 2. 移除通知监听
[[NSNotificationCenter defaultCenter] removeObserver:self];
// 3. 停止 Timer
[self.refreshTimer invalidate];
// 4. 取消网络请求
[self.dataTask cancel];
// ARC 下不需要 [super dealloc]
}
# 4.5.4 MRC vs ARC 对比
手动内存管理与 ARC 的对比
| 特性 | 手动内存管理 | ARC |
|---|---|---|
| 引用计数管理 | 手动调用 retain、release | 编译器自动管理 |
| 代码复杂度 | 较高,容易出错 | 较低,减少内存管理错误 |
| 性能 | 需要手动优化 | 编译器优化,性能较好 |
| 适用场景 | 需要精细控制内存的场合 | 大多数现代 Objective-C 项目 |
# 4.6 循环引用排查与实战
循环引用(Retain Cycle)是 ObjC 开发中最隐蔽的内存泄漏来源,单靠眼睛看代码很难发现。
# 4.6.1 循环引用的三种经典形态
形态一:对象间互相强引用。 A 持有 B 的 strong 引用,B 又持有 A 的 strong 引用,两者永远无法释放。
@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end
@interface Child : NSObject
@property (nonatomic, strong) Parent *parent; // ⚠️ 循环引用
@end
修复:将其中一方改为 weak:
@property (nonatomic, weak) Parent *parent; // ✅ 打破循环
形态二:Block 捕获 self。 对象持有 Block,Block 内部又强引用了该对象。
self.completionBlock = ^{
[self doSomething]; // ⚠️ self → block → self
};
修复:使用 __weak 打破强引用链:
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
[weakSelf doSomething]; // ✅ 弱引用打破循环
};
形态三:NSTimer 持有 target。 Timer 被 RunLoop 持有,Timer 又强引用 target,导致 target 无法释放。这是最容易被忽略的循环引用。
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self // ⚠️ timer → self,RunLoop → timer
selector:@selector(tick)
userInfo:nil
repeats:YES];
修复:使用 iOS 10+ 的 Block API,配合 weak-strong dance:
__weak typeof(self) weakSelf = self;
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *timer) {
[weakSelf tick];
}];
# 4.6.2 weak-strong dance 详解
上面的 __weak 写法有一个小风险:如果在 Block 执行过程中 self 被释放,weakSelf 会变成 nil,导致 Block 内所有操作都静默失效。如果希望在 Block 执行期间保证 self 不被释放,使用 weak-strong dance:
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // 已经释放,安全退出
[strongSelf doSomething]; // 此时 strongSelf 保证存活到 Block 结束
[strongSelf doAnotherThing];
};
原理:strongSelf 是一把"临时的强引用锁"——在 Block 执行期间临时增加引用计数,Block 结束时自动释放。这保证了操作要么完整执行,要么安全跳过,不会出现"做了一半对象没了"的尴尬状态。
# 4.6.3 使用 Xcode 排查循环引用
Xcode 提供了两个强大工具排查内存泄漏:
Instruments - Leaks:Product → Profile → Leaks,运行 App 反复进出可疑页面,观察是否有内存持续增长不回落。
Memory Graph Debugger:运行 App 后点击 Xcode 底部调试栏的"三叉戟"图标,可查看当前所有存活对象及其引用关系。循环引用会显示为两个对象之间互相粗线连接。
实战经验:排查步骤——①先怀疑所有 Block 内部是否用了 self.xxx;②检查所有 delegate 是否声明为 weak;③检查所有 NSTimer 是否正确 invalidate。90% 的内存泄漏逃不出这三个步骤。
# 4.7 ARC 下的常见坑
即使开启了 ARC,仍有一些容易翻车的场景需要手动处理。
坑一:CF 对象和 NS 对象的桥接。 Core Foundation 的 C 对象不受 ARC 管理,需要手动桥接。
// 创建 CF 对象后需要手动用 CFRelease 释放
CFStringRef cfStr = CFStringCreateWithCString(NULL, "hello", kCFStringEncodingUTF8);
NSString *nsStr = (__bridge_transfer NSString *)cfStr; // 所有权转移给 ARC
// 或使用 __bridge 不转移所有权,但需要手动 CFRelease(cfStr);
三种桥接关键字对比:
| 关键字 | 所有权 | 需要手动释放 |
|---|---|---|
__bridge | 不转移 | ✅ 需要 CFRelease |
__bridge_transfer | 转移给 ARC | ❌ ARC 自动管理 |
__bridge_retained | 转移给 CF | ✅ 需要 CFRelease |
坑二:performSelector 的内存警告。 ARC 无法确定 performSelector: 返回值的所有权,编译时会警告。尽量避免使用,改用直接调用或 Block。
坑三:大量临时对象不释放。 循环内创建大量临时对象,即使 ARC 也会在 autorelease pool drain 时才释放。使用 @autoreleasepool 包裹循环体可及时释放:
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d", i];
// ... 使用 str
} // 每次循环结束时释放 str
}
# 4.8 实战案例:内存泄漏排查
假设你发现 App 在反复进出"个人中心"页面后内存持续增长。排查步骤如下:
- 用 Memory Graph 发现存在多个未释放的
UserProfileVC实例。 - 点到其中一个,查看引用链:
UserProfileVC↔UserViewModel↔NetworkManager(发现三者互相持有)。 - 定位代码:
UserProfileVC持有UserViewModel(strong),UserViewModel的回调 Block 捕获了self(即UserProfileVC),NetworkManager的 completion handler 又强引用了UserViewModel。 - 修复:将
UserViewModel对 VC 的引用改为weak;回调 Block 内使用 weak-strong dance;NetworkManager的 handler 改为在回调后置 nil。 - 验证:再次运行 Memory Graph,
UserProfileVC在 pop 后正确释放,内存不再增长。
# 4.9 autorelease 机制深入
# 4.9.1 autorelease 的时机
autorelease 对象在什么时候被真正释放?
每次 RunLoop 迭代结束时:
1. RunLoop 进入休眠前(BeforeWaiting):drain 当前的 autorelease pool
2. 或显式的 @autoreleasepool {} 结束时
典型场景:
主线程:每个 Event Loop 周期结束 → 释放一批 autorelease 对象
子线程:如果没有显式创建 @autoreleasepool,autorelease 对象永不释放(泄漏!)
子线程 autorelease 陷阱:
// ❌ 子线程中没有 autorelease pool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// [NSString stringWithFormat:] 返回 autorelease 对象
// 但此线程没有 autorelease pool → 内存泄漏 + 控制台警告
NSString *s = [NSString stringWithFormat:@"%d", 42];
});
// ✅ 加上 @autoreleasepool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
NSString *s = [NSString stringWithFormat:@"%d", 42];
}
});
# 4.9.2 主线程的自动释放池
主线程的 autorelease pool 由 RunLoop 自动管理——每个 Event Loop 迭代都包含创建和排干自动释放池的步骤。这就是为什么主线程上的 autorelease 对象"看起来"会自动消失。
下一篇预告:ObjC 编程入门完成——接下来可以进入 Swift 编程入门 或回到 模块总览。