Objective-C内存管理(MRC/ARC)看这篇就懂了
引言:为何内存管理如此重要?
在软件开发的世界里,内存管理是一个永恒的话题。它如同一个国家的财政系统,管理不善会导致资源枯竭(内存泄漏),或系统崩溃(野指针访问)。对于 Objective-C 这门建立在 C 语言之上的语言而言,内存管理更是其核心精髓之一。从早期的手动管理(MRC)到后来的自动管理(ARC),苹果公司不断努力简化开发者的负担,但其底层的思想——“引用计数”(Reference Counting)——却从未改变。
理解 Objective-C 的内存管理,不仅是面试中的高频考点,更是编写出健壮、高效、无泄漏 iOS 或 macOS 应用的基石。许多开发者可能直接从 ARC 时代入门,对 MRC 感到陌生,但这恰恰是理解 ARC 工作原理的钥匙。本文将带您穿越时空,从最基本的内存分区讲起,深入剖析 MRC 的黄金法则,最终揭开 ARC 的神秘面纱,让您真正做到“知其然,并知其所以然”。
第一部分:内存管理的基石——栈与堆
要谈内存管理,首先必须理解程序运行时内存是如何划分的。在 Objective-C(以及大多数编程语言)中,内存主要分为两个区域:栈(Stack)和堆(Heap)。
-
栈(Stack)
- 管理者:由编译器自动管理,无需我们操心。
- 存储内容:主要存放局部变量、函数参数、方法调用的地址等。例如,
int a = 10;
,这个变量a
就存放在栈上。 - 特点:
- 高效:栈的内存分配和回收速度极快,因为它是一个“后进先出”(LIFO)的数据结构,分配和释放只是移动栈顶指针。
- 自动:当一个函数或方法执行完毕,其在栈上创建的所有局部变量都会被自动销毁。
- 空间有限:栈的可用空间相对较小,不适合存储大型数据或生命周期需要跨越多个方法作用域的数据。
-
堆(Heap)
- 管理者:由开发者“手动”或“间接”管理。这正是我们内存管理的主战场。
- 存储内容:所有通过
alloc
、new
、copy
等方法创建的 Objective-C 对象都存放在堆上。 - 特点:
- 灵活:堆空间远大于栈,可以动态地申请和释放任意大小的内存。
- 生命周期长:对象的生命周期不局限于某个方法或作用域,只要有“人”还在使用它,它就会一直存在。
- 管理复杂:正是因为其灵活性,堆内存的管理也变得复杂。如果申请了内存而忘记释放,就会造成内存泄漏(Memory Leak);如果内存已经被释放,但仍然试图去访问它,就会导致野指针(Dangling Pointer)问题,引发程序崩溃。
核心问题:我们如何知道一个堆上的对象何时不再被需要,可以安全地释放它呢?Objective-C 的答案是:引用计数(Reference Counting)。
引用计数:一个生动的比喻
想象一下,堆中的一个对象是一本图书馆里的珍贵图书。
* alloc
/new
:图书馆购入一本新书,这本书至少有一个“馆藏”引用,所以它的“被借阅数”(引用计数)为 1。
* retain
(持有):你(一个指针)想借阅这本书,于是办理了借阅手续。这本书的“被借阅数”加 1。
* release
(释放):你读完了,把书还给图书馆。这本书的“被借阅数”减 1。
* dealloc
(销毁):当这本书的“被借阅数”变为 0 时,意味着没有任何人(包括馆藏)在借阅它了。图书馆管理员就会将这本书下架销毁,腾出空间。
这个“被借阅数”就是对象的引用计数(Retain Count)。Objective-C 的运行时系统通过追踪每个对象的引用计数,来决定其何时应该被销毁。
第二部分:手动挡的时代——MRC (Manual Retain-Release)
在 ARC 出现之前,开发者需要像开手动挡汽车一样,精确地控制每一个对象的“离合”与“挂挡”——也就是手动调用 retain
, release
, autorelease
。这套规则的核心思想是“谁创建,谁持有;谁持有,谁释放”。
MRC 的黄金法则
苹果官方文档总结了四条黄金法则,是所有 MRC 操作的根本依据:
-
自己生成的对象,自己持有 (You own any object you create)。
- 当你使用以
alloc
,new
,copy
,mutableCopy
开头的方法创建了一个对象时,你就拥有了这个对象,其引用计数为 1。你必须在未来某个时刻负责释放它。
“`objc
// 你通过 alloc 创建了 str,所以你拥有它。
// str 的引用计数为 1。
NSString *str = [[NSString alloc] initWithFormat:@”Hello”];// … 使用 str …
// 你不再需要 str,你有责任释放它。
[str release];
“` - 当你使用以
-
通过
retain
持有对象 (You take ownership of an object by retaining it)。- 当你从其他地方(如方法返回值、参数等)获得一个对象,而你希望在自己的作用域内长期使用它时,你需要调用
retain
来声明你对它的所有权。这会将对象的引用计数加 1。
“`objc
// 假设 otherObject 是别人传给我们的,我们不拥有它。
// 为了确保在我们的方法执行期间它不被释放,我们 retain 它。
[otherObject retain];
// 现在 otherObject 的引用计数 +1,我们也拥有了它。// … 使用 otherObject …
// 当我们用完后,同样需要释放我们的所有权。
[otherObject release];
“` - 当你从其他地方(如方法返回值、参数等)获得一个对象,而你希望在自己的作用域内长期使用它时,你需要调用
-
当你不再需要你持有的对象时,必须释放它 (You must relinquish ownership of objects you own)。
- 对于你所拥有的每一个对象(无论是通过创建还是
retain
获得),都必须在不再需要它时调用release
或autorelease
来放弃所有权。
- 对于你所拥有的每一个对象(无论是通过创建还是
-
不要释放你不持有的对象 (You must not relinquish ownership of an object you do not own)。
- 如果你没有通过上述方式持有某个对象,那么你绝对不能
release
它。这样做会导致过度释放,当引用计数减到负数时,程序会崩溃。
- 如果你没有通过上述方式持有某个对象,那么你绝对不能
MRC 实践:dealloc
与属性
-
dealloc
方法:这是对象生命周期的终点。当一个对象的引用计数降为 0 时,系统会自动调用其dealloc
方法。在 MRC 中,dealloc
的核心任务是释放该对象所持有的其他对象。“`objc
– (void)dealloc {
// 释放你所持有的实例变量
[_myString release];
[_myArray release];// 必须调用父类的 dealloc,这是规定! [super dealloc];
}
“` -
属性(Property):在 MRC 时代,属性的
setter
方法是体现内存管理逻辑的关键场所。assign
:用于基本数据类型(int
,float
,BOOL
)和非对象类型的指针。它只是简单的赋值,不涉及任何引用计数操作。retain
:用于对象类型。setter
方法会自动为你处理“先release
旧值,再retain
新值”的逻辑,以保证所有权正确转移且不发生泄漏。
“`objc
@property (nonatomic, retain) NSString *name;// 上面的声明,编译器会自动生成(或我们可以手动实现)如下的 setter 方法:
– (void)setName:(NSString *)newName {
if (_name != newName) {
[_name release]; // 释放旧值的所有权
_name = [newName retain]; // 持有新值的所有权
}
}
“`
autorelease
与 NSAutoreleasePool
autorelease
是 MRC 中一个巧妙但略显复杂的机制。它表示“我暂时不想释放这个对象,但请在未来的某个时刻帮我释放它”。
-
使用场景:当一个方法需要创建一个对象并将其返回给调用者时,它不能在方法内部
release
这个对象(否则调用者会收到一个被释放的野指针),但它又不希望调用者必须记住去release
。“`objc
// 一个典型的工厂方法
+ (NSString )createMyString {
NSString str = [[NSString alloc] initWithFormat:@”I am a temporary string”];// 我们创建了 str,但要把它返回给别人。 // 如果直接返回,调用者就得负责 release。 // 如果在这里 release,返回的就是野指针。 // 正确做法是 autorelease。 return [str autorelease]; // 这会将 str 放入当前的“自动释放池”。 // str 的所有权被转移给了池子,它的引用计数暂时不变。
}
“` -
NSAutoreleasePool
:自动释放池。所有被标记为autorelease
的对象,都会被添加到离它最近的NSAutoreleasePool
中。当这个池子被“抽干”(drain)或销毁时,它会向池中所有的对象发送一条release
消息。- 在 iOS 应用中,主线程的 RunLoop 会在每个事件循环的开始创建一个
NSAutoreleasePool
,并在事件循环结束时将其抽干。这就是为什么大部分情况下我们感觉不到池子的存在。 - 在需要大量创建临时对象的循环中,手动创建
NSAutoreleasePool
可以有效防止内存峰值过高。
- 在 iOS 应用中,主线程的 RunLoop 会在每个事件循环的开始创建一个
第三部分:自动挡的革命——ARC (Automatic Reference Counting)
MRC 功能强大,但完全依赖于开发者的严谨和细心。稍有不慎,retain
和 release
的不匹配就会导致内存泄漏或崩溃。为了解放生产力,苹果在 iOS 5 和 OS X 10.7 中引入了 ARC。
重要认知:ARC 不是垃圾回收(GC)!
ARC 不是像 Java 或 C# 那样的垃圾回收器,它没有一个后台线程在扫描和回收垃圾。ARC 的本质是,编译器在编译阶段,自动为你插入了所有必要的 retain
, release
, autorelease
调用。 换句话说,ARC 只是把 MRC 的那些黄金法则自动化了。你写的代码表面上干净了,但编译后的代码和精心编写的 MRC 代码几乎一样。
ARC 下的变化
- 禁止手动调用:在 ARC 环境下,你不能再调用
retain
,release
,autorelease
,retainCount
,也不能调用dealloc
(但可以重写dealloc
来释放非对象资源,如 Core Foundation 对象、文件句柄等,只是不能调用[super dealloc]
)。 - 新的属性关键字:
strong
:替代了 MRC 的retain
。表示一个“强引用”,只要有强引用指向一个对象,这个对象就不会被销毁。这是对象属性的默认值。weak
:表示一个“弱引用”。它指向一个对象,但不会增加其引用计数。最关键的是,当弱引用指向的对象被销毁后,该弱引用会自动被置为nil
。这是 ARC 解决“循环引用”问题的核心武器。unsafe_unretained
:类似weak
,也是弱引用,不增加引用计数。但区别在于,当它指向的对象被销毁后,它不会被置为nil
,而是变成一个野指针。除非为了兼容老版本系统或特殊性能要求,否则应优先使用weak
。assign
: 依然用于基本数据类型。
ARC 的核心任务:解决循环引用(Retain Cycle)
这是 ARC 时代开发者最需要关注的内存问题。
-
什么是循环引用?
两个或多个对象互相强引用对方,导致它们的引用计数都无法降为 0,从而谁也无法被释放,造成内存泄漏。 -
典型场景:Delegate 模式
一个ViewController
持有一个UITableView
,UITableView
又需要一个delegate
和dataSource
来提供数据和处理事件,通常这个delegate
就是ViewController
自己。“`objc
// ViewController.h
@interface MyViewController : UIViewController
@property (nonatomic, strong) UITableView *tableView; // VC 强引用 tableView
@end// ViewController.m
– (void)viewDidLoad {
[super viewDidLoad];
self.tableView = [[UITableView alloc] init];
self.tableView.delegate = self; // tableView 强引用了 VC
// 此时,VC -> tableView, tableView -> VC,循环引用形成!
}
``
MyViewController
在这个例子中,的
tableView属性是
strong的,它强引用了
tableView对象。同时,
tableView的
delegate属性在系统实现中也是
strong的(在 ARC 之前是
retain),它强引用了
MyViewController` 实例。这样一来,两个对象互相“抓住”对方,谁也无法在对方被释放前释放,导致双双泄漏。 -
解决方案:
weak
打破循环引用的关键在于,将循环链条中的一环从强引用(strong
)改为弱引用(weak
)。通常遵循“谁拥有谁,谁就强引用;谁只是使用谁,谁就弱引用”的原则。在 Delegate 模式中,
ViewController
“拥有”tableView
,所以它对tableView
的引用是strong
是合理的。而tableView
只是“使用”ViewController
作为其代理,不应该拥有它。因此,delegate
属性的设计就应该是weak
的。事实上,苹果 SDK 中所有的
delegate
和dataSource
属性都已经被声明为weak
(或在 MRC 下的assign
)。objc
// UIKit 中 UITableView 的 delegate 属性声明
@property (nonatomic, weak, nullable) id<UITableViewDelegate> delegate;
因此,我们日常开发中遇到的delegate
循环引用问题,通常是我们自定义类时忘记将delegate
属性声明为weak
。“`objc
// 正确的自定义 delegate 声明
@protocol MyCustomClassDelegate
– (void)customClassDidFinishTask:(MyCustomClass *)sender;
@end@interface MyCustomClass : NSObject
// 代理属性必须声明为 weak,以防循环引用
@property (nonatomic, weak) iddelegate;
@end
``
self
* **另一个常见场景:Block**
Block 内部如果使用了,会导致 Block 捕获并强引用
self。如果此时
self` 也强引用了这个 Block(比如把 Block 存为一个属性),循环引用就产生了。objc
// 在一个 ViewController 内部
self.myBlock = ^{
// Block 强引用了 self
NSLog(@"My name is %@", self.name);
};
// self.myBlock 强引用了 Block
// self -> Block -> self 循环引用解决方案:使用
weak-strong dance
。objc
__weak typeof(self) weakSelf = self; // 创建一个 self 的弱引用
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 在 Block 内部转为强引用
if (strongSelf) {
// Block 内部使用 strongSelf,确保在 Block 执行期间 self 不会被释放
NSLog(@"My name is %@", strongSelf.name);
}
};
这样,self
对 Block 是强引用,但 Block 对self
只是弱引用 (weakSelf
),打破了循环。Block 内部的strongSelf
只是一个局部变量,当 Block 执行完毕后就会释放,它保证了在 Block 执行的瞬间self
是存在的,防止了多线程环境下可能出现的问题。
第四部分:总结与展望
特性 | MRC (Manual Retain-Release) | ARC (Automatic Reference Counting) |
---|---|---|
核心思想 | 开发者手动管理对象所有权 | 编译器自动管理对象所有权 |
关键操作 | retain , release , autorelease |
无,由编译器自动插入 |
属性关键字 | assign , retain , copy |
assign , strong , weak , unsafe_unretained , copy |
dealloc |
必须调用 [super dealloc] ,释放持有的对象 |
不能调用 [super dealloc] ,用于释放非 OC 资源 |
主要挑战 | 忘记 release 导致泄漏,过度 release 导致崩溃 |
循环引用(Retain Cycle)导致的内存泄漏 |
解决方案 | 遵循黄金法则,细心编码 | 使用 weak 关键字打破强引用环,尤其在 delegate 和 block 中 |
从 MRC 到 ARC,是 Objective-C 开发的一次巨大飞跃。它极大地降低了内存管理的门槛,让开发者能更专注于业务逻辑。然而,这并不意味着我们可以完全忽视内存管理。ARC 只是将繁琐的手动操作自动化,但其背后的“引用计数”和“所有权”思想依然是根基。
要成为一名优秀的 iOS/macOS 开发者,深刻理解 MRC 的工作原理是必不可少的。它能让你在面对 ARC 下的循环引用问题时,能从根源上分析并解决问题,而不是仅仅记住“delegate 要用 weak”这样的表层规则。希望通过本文的梳理,您已经对 Objective-C 的内存管理有了一个清晰而立体的认识,无论是面对老旧的 MRC 项目,还是在现代的 ARC 开发中,都能游刃有余。