ObjC内存管理
# 04.ObjC内存管理
# 目录介绍
- 4.1 生命周期设计
- 4.1.1 初始化操作
- 4.1.2 销毁时释放
- 4.2 内存管理
- 4.2.1 引用计数
- 4.2.2 引用计数规则
- 4.2.3 内存管理方式
- 4.3 手动引用计数
- 4.3.1 MRC案例
- 4.3.2 MRC案例分析
- 4.3.3 原理设计
- 4.3.4 使用自动释放池
- 4.4 自动引用计数
- 4.4.1 ARC案例
- 4.4.2 ARC案例分析
- 4.4.3 原理设计
- 4.5 内存最佳实践
- 4.5.1 避免循环引用
- 4.5.2 管理临时对象
- 4.5.3 dealloc
- 4.5.4 MRC vs ARC
# 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方法先调用
}
# 4.1.2 销毁时释放
1.释放(Deallocation):在释放阶段,对象不再被使用,内存空间将被释放。当对象不再被引用时,Objective-C的自动引用计数(ARC)机制会自动处理对象的释放。
obj = nil; // 对象将被释放
2.释放前的清理(Cleanup before Deallocation):在对象被释放之前,可以在dealloc方法中执行一些清理操作,如释放资源、取消观察者、解除通知等。
// 对应于 initialize 方法,用于清空对象,当引用计数为 0 时被调用
- (void)dealloc {
// 如果未启用 ARC,则需要手动释放引用计数
[nickName release];
// 清理操作
[super dealloc];
}
# 4.2 内存管理
# 4.2.1 引用计数
引用计数是 Objective-C 内存管理的核心机制。每个对象都有一个引用计数器,用于记录当前有多少个对象在使用它。
# 4.2.2 引用计数规则
- 当对象被创建时,引用计数为 1。
- 当对象被其他对象引用时,引用计数加 1。
- 当对象不再被引用时,引用计数减 1。
- 当引用计数为 0 时,对象被销毁,内存被释放。
# 4.2.3 内存管理方式
内存管理是一项重要的任务,用于管理对象的生命周期和内存分配。在Objective-C中,有两种主要的内存管理方式:
手动引用计数(Manual Reference Counting,MRC)和自动引用计数(Automatic Reference Counting,ARC)。
# 4.3 手动引用计数
# 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,对象被销毁
# 4.3.3 原理设计
# 4.3.4 使用自动释放池
自动释放池用于延迟对象的释放。当对象被发送 autorelease 消息时,它会被添加到当前的自动释放池中。当自动释放池被释放时,池中的所有对象都会收到 release 消息。
@autoreleasepool {
// 创建对象并添加到自动释放池
NSString *str = [[[NSString alloc] initWithFormat:@"Hello, %@", @"World"] autorelease];
// 使用对象
NSLog(@"%@", str);
// 自动释放池结束时,str 会被释放
}
# 4.4 自动引用计数
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
# 4.4.2 ARC案例分析
ARC 的规则
- 编译器会自动在适当的位置插入
retain、release和autorelease。 - 开发者不能手动调用
retain、release和autorelease。 - 对象的生命周期由编译器管理。
# 4.4.3 原理设计
ARC:编译器自动插入 retain/release(默认模式)
属性修饰符:
strong:持有对象(默认)weak:弱引用(自动置 nil)copy:复制对象(用于 NSString/NSArray)assign:基本数据类型(非对象)
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.手动释放内存:在MRC中,需要手动释放对象的内存,可以在适当的时机调用release方法。在ARC中,不需要手动释放内存,编译器会自动插入释放代码。
# 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];
}
# 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);
坑二: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 后正确释放,内存不再增长。