【碎片八股文 #003】ARC 为什么能自动管理内存?

70 阅读5分钟

【碎片八股文 #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,手动引用计数)

特性MRCARC
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 和垃圾回收的区别

特性ARCGC(Java/C#)
工作时机编译期运行时
释放时机引用计数为 0 时立即释放GC 线程定期扫描
性能影响确定性,无停顿可能有 STW(Stop The World)
循环引用需要手动用 weak 打破GC 自动检测

核心区别:

  • ARC 是 编译期优化,运行时开销小
  • GC 是 运行时机制,需要额外线程扫描

六、图解核心概念

强引用和弱引用

┌─────────────┐
│   对象A      │  retainCount = 2
└─────────────┘
      ▲    ▲
      │    │
  strong  weak
      │    │
   变量1  变量2

// 当变量1 释放后
retainCount = 1weak 不影响计数)

// 当对象 A 被释放后
变量2 自动变为 nilweak 的特性)

循环引用问题

┌─────────────┐         ┌─────────────┐
│   对象 A     │         │   对象 B    │
│             │ strong  │             │
│   持有 B ────┼────────→│             │
│             │         │   持有 A ────┼────┐
└─────────────┘         └─────────────┘    │
      ▲                                    │
      │             strong                 │
      └────────────────────────────────────┘

// AB 互相强引用,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)中。

当对象释放时:

  1. Runtime 查找弱引用表
  2. 把所有指向该对象的 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 是如何从点击图标到启动界面的?