跳到主要内容

内存管理

问题

OC 的内存管理机制经历了哪些演变?ARC 的工作原理是什么?Autorelease Pool 的作用和时机?

答案

MRC 到 ARC 的演变

MRCARC
全称Manual Reference CountingAutomatic Reference Counting
管理方式手动 retain/release编译器自动插入 retain/release
引入版本iOS 2iOS 5
引用计数手动维护编译器 + Runtime 维护
dealloc 调用[super dealloc]不需要调 super

引用计数原理

// MRC 时代
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj retain]; // retainCount = 2
[obj release]; // retainCount = 1
[obj release]; // retainCount = 0 → dealloc

引用计数存储位置:

  • retainCount < 某阈值:存在 isa 指针的 extra_rc 位域中(nonpointer isa)
  • retainCount 过大时:溢出部分存到 SideTable 的 refcnts 哈希表中

ARC 规则

ARC 是编译器特性,在编译时自动插入内存管理代码:

// 你写的代码
- (void)viewDidLoad {
NSObject *obj = [[NSObject alloc] init];
self.myObj = obj;
}

// ARC 编译器实际生成的(伪代码)
- (void)viewDidLoad {
NSObject *obj = [[NSObject alloc] init]; // +1
[self.myObj release]; // 旧值 -1
[obj retain]; // 新值 +1
self.myObj = obj;
[obj release]; // 局部变量出作用域 -1
}

属性内存管理修饰符

修饰符引用计数使用场景
strong+1默认,强引用持有对象
weak不变避免循环引用(delegate 等)
assign不变基本类型(int、BOOL 等)
copy+1(拷贝副本)NSString、Block
unsafe_unretained不变类似 weak 但不置 nil

weak 的实现原理

weak 置 nil 过程
  1. 对象引用计数变为 0,调用 dealloc
  2. Runtime 从 SideTableweak_table 中找到所有指向该对象的 weak 指针
  3. 将这些指针全部置为 nil
  4. weak_table 中移除记录

Autorelease Pool

// 使用场景:延迟释放
- (NSString *)fullName {
NSString *result = [NSString stringWithFormat:@"%@ %@",
self.firstName, self.lastName];
// result 被 autorelease,调用者可以安全使用
return result;
}

// 自定义 Autorelease Pool
@autoreleasepool {
for (int i = 0; i < 100000; i++) {
NSString *str = [NSString stringWithFormat:@"%d", i];
// str 在 @autoreleasepool 结束时释放
// 避免内存峰值过高
}
}

Autorelease Pool 的底层

  • 底层结构是 AutoreleasePoolPage(双向链表)
  • 每个 page 4096 字节
  • @autoreleasepool {} 对应 push() / pop() 操作
  • RunLoop 每次循环结束会 poppush 新的 pool

RunLoop 与 Autorelease 的关系


常见面试问题

Q1: ARC 下还会有内存泄漏吗?

答案:会。ARC 管理的是引用计数,以下情况仍会泄漏:

  1. 循环引用:A → B → A(需用 weak 打破)
  2. Block 循环引用:self → block → self
  3. NSTimer 强引用:target 被强引用导致 VC 无法释放
  4. Delegate 设为 strong
  5. Core Foundation 对象:CF 对象不受 ARC 管理,需要手动 CFRelease
  6. C 语言 malloc:需手动 free

Q2: weak 和 assign 的区别?

答案

weakassign
适用范围OC 对象基本类型或对象
对象释放后自动置 nil指针不变(野指针)
性能开销略高(维护 weak 表)无额外开销

assign 修饰对象会导致野指针崩溃,所以对象应该用 weak

Q3: 什么时候需要手动创建 @autoreleasepool?

答案

// 1. 大量临时对象的循环
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d", i];
// 每次循环结束就释放,避免内存飙升
}
}

// 2. 非主线程(没有默认 RunLoop 的 Autorelease Pool)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
// 后台线程操作
}
});

Q4: retain/release 的底层实现?

答案

引用计数存储在两个地方(nonpointer isa 优化):

  • isa 的 extra_rc 位域:存储额外引用计数(8 位或 19 位,取决于架构)
  • SideTable 的 refcnts:extra_rc 溢出时,一半转存到 SideTable

retain 操作:先尝试对 isa 的 extra_rc 做原子 +1,溢出则转存到 SideTable。 release 操作:先尝试对 extra_rc 做原子 -1,如果为 0 则检查 SideTable,引用计数归 0 触发 dealloc。

相关链接