【碎片八股文 #003】ARC 为什么能自动管理内存?
一、面试题原文
面试官: iOS 的 ARC 是怎么实现自动内存管理的?它和垃圾回收(GC)有什么区别?
候选人: ARC 会自动释放对象吧……不需要手动 release。
面试官心里想: 能说出引用计数、编译期插入代码、循环引用就算过关了。
二、常见误答
很多人只知道"ARC 能自动管理内存",但说不清楚:
- ARC 的"自动"是怎么实现的?
- 为什么 ARC 不是垃圾回收?
- 为什么还会有内存泄漏?
这些都是面试官会追问的点。
三、正确理解
什么是 ARC?
ARC(Automatic Reference Counting,自动引用计数) 是 iOS 的内存管理机制。
核心原理:
- 每个对象都有一个 引用计数(retainCount)
- 当引用计数降为 0 时,对象自动释放
- 编译器在编译期自动插入 retain/release 代码
ARC 和 MRC 的区别
在 ARC 之前,iOS 使用 MRC(Manual Reference Counting,手动引用计数) 。
| 特性 | MRC | ARC |
|---|---|---|
| retain/release | 开发者手动调用 | 编译器自动插入 |
| 内存管理责任 | 开发者负责 | 编译器负责 |
| 出错概率 | 高(容易忘记释放) | 低(编译器保证) |
关键点: ARC 不是运行时的垃圾回收,而是 编译期的代码自动生成。
四、ARC 的核心原理
1. 引用计数如何工作?
每个对象内部都有一个计数器:
创建对象 retainCount = 1
被强引用 retainCount + 1
引用解除 retainCount - 1
计数为 0 对象被释放
示例:
// MRC 时代需要手动管理
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj retain]; // retainCount = 2
[obj release]; // retainCount = 1
[obj release]; // retainCount = 0,对象释放
// ARC 时代编译器自动处理
NSObject *obj = [[NSObject alloc] init]; // 编译器自动在合适时机插入 release
// 离开作用域时,编译器自动调用 release
2. 编译器如何插入代码?
ARC 的"自动"本质是 编译器在编译期分析代码,自动插入 retain/release/autorelease 调用。
编译前(开发者写的代码):
- (void)test {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%@", obj);
}
编译后(编译器生成的代码):
- (void)test {
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
NSLog(@"%@", obj);
objc_release(obj); // 编译器自动插入,retainCount = 0
}
关键点: retain/release 的调用时机是 编译期确定的,不是运行时动态判断。
3. 所有权修饰符
ARC 通过 所有权修饰符 控制对象的生命周期:
| 修饰符 | 作用 | 引用计数变化 |
|---|---|---|
| __strong | 强引用(默认) | +1 |
| __weak | 弱引用 | 不变(不增加计数) |
| __unsafe_unretained | 不安全的弱引用 | 不变(对象释放后成野指针) |
| __autoreleasing | 自动释放池 | 延迟释放 |
常见用法:
// 强引用(默认)
NSObject *obj1 = [[NSObject alloc] init]; // retainCount = 1
// 弱引用(不增加计数)
__weak NSObject *obj2 = obj1; // retainCount 仍然是 1
// obj1 释放后,obj2 自动变为 nil
obj1 = nil; // obj2 也变成 nil
五、ARC 和垃圾回收的区别
| 特性 | ARC | GC(Java/C#) |
|---|---|---|
| 工作时机 | 编译期 | 运行时 |
| 释放时机 | 引用计数为 0 时立即释放 | GC 线程定期扫描 |
| 性能影响 | 确定性,无停顿 | 可能有 STW(Stop The World) |
| 循环引用 | 需要手动用 weak 打破 | GC 自动检测 |
核心区别:
- ARC 是 编译期优化,运行时开销小
- GC 是 运行时机制,需要额外线程扫描
六、图解核心概念
强引用和弱引用
┌─────────────┐
│ 对象A │ retainCount = 2
└─────────────┘
▲ ▲
│ │
strong weak
│ │
变量1 变量2
// 当变量1 释放后
retainCount = 1(weak 不影响计数)
// 当对象 A 被释放后
变量2 自动变为 nil(weak 的特性)
循环引用问题
┌─────────────┐ ┌─────────────┐
│ 对象 A │ │ 对象 B │
│ │ strong │ │
│ 持有 B ────┼────────→│ │
│ │ │ 持有 A ────┼────┐
└─────────────┘ └─────────────┘ │
▲ │
│ strong │
└────────────────────────────────────┘
// A 和 B 互相强引用,retainCount 都无法降为 0
// 导致内存泄漏
解决方案: 一侧用 weak 打破循环
@interface ClassA : NSObject
@property (strong, nonatomic) ClassB *b;
@end
@interface ClassB : NSObject
@property (weak, nonatomic) ClassA *a; // 用 weak 打破循环
@end
七、延伸提问
1. 为什么 ARC 下还会有内存泄漏?
常见场景:
场景 1: 循环引用
// Block 捕获 self,self 又持有 block
self.block = ^{
[self doSomething]; // 循环引用
};
// 解决方案:用 weak-strong dance
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
场景 2: CoreFoundation 对象
// CF 对象不受 ARC 管理,需要手动释放
CFStringRef str = CFStringCreateWithCString(NULL, "test", kCFStringEncodingUTF8);
// 必须手动释放
CFRelease(str);
场景 3: NSTimer
// Timer 会强引用 target
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(update)
userInfo:nil
repeats:YES];
// 必须手动 invalidate
[self.timer invalidate];
2. weak 是如何实现的?
weak 指针会被注册到一个全局的 弱引用表(weak table)中。
当对象释放时:
- Runtime 查找弱引用表
- 把所有指向该对象的 weak 指针设为 nil
这就是为什么 weak 指针在对象释放后会自动变 nil。
3. autorelease 是什么?
autorelease 是延迟释放机制,对象不会立即释放,而是加入 自动释放池(autoreleasepool)。
@autoreleasepool {
NSObject *obj = [[[NSObject alloc] init] autorelease];
// obj 在这里还活着
} // 离开作用域时,池子排干,obj 被释放
典型场景: 方法返回临时对象时
- (NSString *)getName {
NSString *name = [[NSString alloc] initWithFormat:@"User"];
return name; // 编译器自动加 autorelease
}
4. ARC 下能手动调用 retain/release 吗?
不能。 ARC 模式下,编译器会报错:
[obj retain]; // 编译错误:ARC forbids explicit message send of 'retain'
[obj release]; // 编译错误:ARC forbids explicit message send of 'release'
如果需要混合使用 MRC 代码,可以用 -fno-objc-arc 编译选项。
八、记忆口诀
"ARC 编译插代码,引用计数自动搞;强引用加一,弱引用不扰;循环引用要打破,weak 来帮忙。"
九、碎片笔记
核心关键词: ARC、引用计数、编译期插入、strong、weak、循环引用
重点记忆:
- ARC 是编译期自动插入 retain/release,不是运行时 GC
- strong 引用会使 retainCount +1,weak 不会
- weak 指针在对象释放后自动变 nil
- 循环引用需要用 weak 打破
实际应用:
- Block 捕获 self 时,用 weak-strong dance 避免循环引用
- NSTimer 使用完必须 invalidate,否则会泄漏
- CoreFoundation 对象不受 ARC 管理,需要手动 CFRelease
今天的碎片,帮你面试少挂一次。
下一篇预告: 【碎片八股文 #004】Activity 是如何从点击图标到启动界面的?