内存管理(ARC/Weak/AutoreleasePool)
面试回答版
一句话概括
ARC(Automatic Reference Counting)是编译器自动插桩的引用计数管理机制,它在编译期将 retain/release/autorelease 操作插入合适位置,不是 GC(垃圾回收),没有后台回收线程。
ARC 基本原理
编译器插桩规则
// 编译器在编译期自动插入 retain/release
// 源代码
- (void)handleObject:(id)obj {
[obj doSomething];
self.property = obj;
}
// 编译后等效
- (void)handleObject:(id)obj {
[obj retain]; // +1:确保方法内有效
[obj doSomething];
[self.property release]; // release 旧值
[obj retain]; // +1
self->_property = obj;
[obj release]; // -1:平衡方法开头的 retain
[obj release]; // -1:平衡参数传入时的 retain
}
-
alloc/new/copy/mutableCopy开头的返回已 +1。 -
retain计数 +1,release计数 -1。 -
ARC 下不允许手动调用
retain/release/autorelease/dealloc。
MRC vs ARC
| 对比项 | MRC | ARC |
|---|---|---|
| retain/release | 手动调用 | 编译器自动插桩 |
| dealloc | 需调 [super dealloc] | 不可手动调,自动链式调用 |
| 自动释放池 | 可选 | 编译器插入 autorelease |
| 弱引用 | 无 | __weak / __unsafe_unretained |
| 方法命名约定 | 无强制 | copy/init/new 等有规则 |
引用计数底层实现
retain / release 路径
[obj retain] → objc_retain(obj) → obj->rootRetain() → SideTable::tryRetain()
→ 引用计数表中 +1
[obj release] → objc_release(obj) → obj->rootRelease() → SideTable::tryRelease()
→ 引用计数表中 -1
→ 归零则调用 dealloc
第一步:从对象找到 SideTable — StripedMap 分片哈希
全局不是一张大表,而是由 StripedMap 管理的 8 张 SideTable(arm64 真机)。用对象地址哈希后取模,分散到不同表里,减少并发锁竞争:
// objc4 运行时源码精简
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 }; // 真机 8 张
#else
enum { StripeCount = 64 }; // 模拟器/其他平台 64 张
#endif
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
};
// 哈希函数:对象指针右移后异或,再取模
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
SideTable& operator[] (const void *p) {
return tables[indexForPointer(p)];
}
SideTable tables[StripeCount];
};
static StripedMap SideTables; // 全局唯一的 8 张 SideTable
查找链路:
对象指针 obj
→ StripedMap::indexForPointer(obj)
→ ((obj >> 4) ^ (obj >> 9)) % 8 // 位运算等价于 & 7
→ SideTables.tables[stripe_index]
→ 锁住该 SideTable 的 slock
→ 在 refcnts / weak_table 中操作该对象
设计要点:每张 SideTable 有独立的自旋锁。不同对象大概率落在不同 Stripe,并发 retain/release 时锁竞争降到 1/8。
第二步:RefcountMap 的位布局 — 引用计数如何存储在哈希表中
RefcountMap = DenseMap<DisguisedPtr<objc_object>, size_t>。key 为对象指针的伪装值,value 为紧凑的位域:
SideTable refcnts value 的 64 位布局:
┌──────────────┬──────────────────────────────────────────┬──────┬──────┐
│ bit 63 │ bit 62 ~ 2 │bit 1 │bit 0 │
│ RC_PINNED │ 引用计数值(每个单位 = SIDE_TABLE_RC_ONE) │DEALLC│WEAKLY│
└──────────────┴──────────────────────────────────────────┴──────┴──────┘
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) // bit 0 = 1:该对象有 weak 引用
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // bit 1 = 2:该对象正在 dealloc
#define SIDE_TABLE_RC_ONE (1UL<<2) // bit 2 = 4:一个引用计数的单位
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1)) // bit 63:引用计数溢出,不再增减
为什么 RC_ONE = 4 而不是 1? 低 2 位被
WEAKLY_REFERENCED和DEALLOCATING两个标记位占用,引用计数只能从 bit 2 开始存储。
真实引用计数 = 1(基础值)+ (bits >> 2)。基础值 1 隐含在逻辑中,不占 bit 位。
retain 的底层操作:
bool SideTable::tryRetain(objc_object *obj) {
auto it = refcnts.find(obj);
if (it == refcnts.end()) return false;
size_t &bits = it->second;
if (bits & SIDE_TABLE_DEALLOCATING) return false; // 正在释放,拒绝 retain
if (!(bits & SIDE_TABLE_RC_PINNED)) {
bits += SIDE_TABLE_RC_ONE; // +4,相当于引用计数 +1
}
// 如果 RC_PINNED,说明计数已爆表,不再增加(该对象永不释放)
return true;
}
release 的底层操作:
bool SideTable::tryRelease(objc_object *obj) {
auto it = refcnts.find(obj);
if (it == refcnts.end()) return false;
size_t &bits = it->second;
bits -= SIDE_TABLE_RC_ONE; // -4,相当于引用计数 -1
if ((bits >> 2) == 0) { // 右移 2 位后为 0 → 引用计数归零
bits |= SIDE_TABLE_DEALLOCATING; // 打上"释放中"标记
return true; // 返回 true → 调用者触发 dealloc
}
return false;
}
第三步:非指针 isa 的 extra_rc — 绝大多数情况无锁操作
arm64 下 isa 不是纯 class 指针,而是一个嵌入多种信息的 nonpointer isa:
非指针 isa 的 64 位布局(arm64):
┌─────────────────────┬──────┬────────┬──────────┬─────────┬──────────────┐
│ class (33b) │magic │dealloc │weakly_ref│has_assoc│ extra_rc 19b │
└─────────────────────┴──────┴────────┴──────────┴─────────┴──────────────┘
-
extra_rc占 19 位,可存 0 ~ 524287 的额外引用计数。 -
retain 快速路径:extra_rc 未满 → 直接
isa.extra_rc++(无锁,无哈希查找)。 -
retain 慢速路径:extra_rc 满了 → 将一半刷入 SideTable 腾空间,再 extra_rc++。
-
release 快速路径:extra_rc > 0 → 直接
isa.extra_rc--。 -
release 慢速路径:extra_rc == 0 → 从 SideTable 借一半到 extra_rc,再减。
完整 retain / release 决策树
retain(obj)
├─ Tagged Pointer?→ 直接返回(值存在指针里,无堆内存无引用计数)
├─ 非指针 isa 且 extra_rc 未满 → isa.extra_rc++(快速路径,无锁)
├─ 非指针 isa 且 extra_rc 已满 → 一半刷入 SideTable,然后 extra_rc++
└─ 非 nonpointer(旧设备/特殊对象)→ 直接走 SideTable
release(obj)
├─ Tagged Pointer?→ 直接返回
├─ 非指针 isa 且 extra_rc > 0 → isa.extra_rc--(快速路径)
├─ 非指针 isa 且 extra_rc == 0 → 从 SideTable 借一半到 extra_rc,再减
└─ 非 nonpointer → 直接走 SideTable
→ refcnts 减后归零?
→ 标记 SIDE_TABLE_DEALLOCATING
→ 调用 dealloc
weak 实现原理
整体架构:四层数据结构
weak 引用不是简单存个指针——运行时维护了一套完整的分层查找结构:
SideTables (全局 StripedMap,8 张 SideTable)
└─ SideTable (第 i 张表)
├─ slock (自旋锁)
├─ refcnts (引用计数 DenseMap)
└─ weak_table (弱引用全局表 — weak_table_t)
├─ weak_entry_t [0] (每个被弱引用的对象对应一个 entry)
├─ weak_entry_t [1]
├─ ...
└─ weak_entry_t [n]
├─ referent (被弱引用的对象)
└─ referrers[] (所有指向该对象的 __weak 指针地址数组)
第一层:从对象找到 weak_table
和引用计数一样,先通过对象地址找 SideTable,然后拿到 weak_table:
obj
→ StripedMap::indexForPointer(obj) // 哈希取模
→ SideTables.tables[i] // 拿到对应 SideTable
→ table->weak_table // 拿到该表下的 weak_table
第二层:weak_table_t 结构
struct weak_table_t {
weak_entry_t *weak_entries; // 哈希数组,每个元素是一个 weak_entry_t
size_t num_entries; // 当前 entry 数量
uintptr_t mask; // 哈希掩码 = 数组容量 - 1
uintptr_t max_hash_displacement; // 最大哈希偏移(用于判定是否需要 rehash)
};
weak_table 是一个开放定址哈希表(open addressing)。通过 referent(被弱引用对象)的地址哈希找到槽位,冲突时线性探测。
第三层:weak_entry_t 结构
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象(哈希 key)
union {
// 方式一:动态外部分配数组(referrer 数量 > 4 时)
struct {
weak_referrer_t *referrers; // 指向动态分配的 referrer 数组
uintptr_t out_of_line_ness : 2; // 标记:使用外部分配
uintptr_t num_refs : 30; // referrer 数量
uintptr_t mask; // 外部分配数组的哈希掩码
uintptr_t max_hash_displacement; // 最大哈希偏移
};
// 方式二:内联存储(referrer 数量 ≤ 4 时,无需额外 malloc)
struct {
weak_referrer_t inline_referrers[4]; // 4 个内联槽位
};
};
};
优化设计:当指向某个对象的 __weak 变量 ≤ 4 个时,referrer 指针直接存在 entry 内部(inline),无需额外分配堆内存。超过 4 个才切换为动态外部分配数组。
第四层:weak_referrer_t
typedef DisguisedPtr<objc_object *> weak_referrer_t;
// 本质就是 __weak id * 的伪装指针 —— 指向那个 __weak 变量自身的地址
storeWeak:注册 weak 引用的完整流程
/**
* 参数:
* location: __weak 变量自身的地址 (id *location)
* newObj: 要弱引用指向的对象
*/
id objc_storeWeak(id *location, id newObj) {
// ===== 第 1 步:清理旧绑定 =====
// 如果 location 之前已经指向了某个旧对象 oldObj
// → 用旧对象地址找 SideTable
// → 从 weak_table 找到 oldObj 对应的 weak_entry_t
// → 从该 entry 的 referrers 数组中移除 location
// → 如果移除后该 entry 的 referrers 为空,删除整个 entry
// ===== 第 2 步:注册新绑定 =====
if (newObj) {
// 用 newObj 地址找 SideTable
SideTable &table = SideTables[newObj];
// 在 weak_table 中查找或创建 newObj 对应的 weak_entry_t
weak_entry_t *entry = weak_entry_for_referent(&table.weak_table, newObj);
// 将 location 追加到 entry 的 referrers 数组中
append_referrer(entry, location);
// 在 refcnts 中标记该对象"有 weak 引用"
table.refcnts[newObj] |= SIDE_TABLE_WEAKLY_REFERENCED;
}
// ===== 第 3 步:赋值 =====
*location = newObj;
return newObj;
}
图解:storeWeak(obj, location) 执行后的内存关系:
__weak id *location (栈上的 weak 变量)
│
│ 指向
▼
[obj] (堆上的 OC 对象)
│
│ StripedMap 哈希
▼
SideTable #3
│
▼
weak_table
│
▼
weak_entry_t { referent = obj, referrers = [location, ...] }
loadWeak:读取 weak 引用的过程
id objc_loadWeak(id *location)
→ objc_loadWeakRetained(location)
→ obj->rootTryRetain()
→ 尝试 retain 该对象
→ 成功,返回 retained 对象
→ 失败(正在 dealloc),返回 nil
→ 将 retained 对象加入 autoreleasepool(确保调用者在当前 RunLoop 内安全使用)
这就是为什么
__weak变量使用前要__strong强引用一下——防止多线程下对象在读取后、使用前被释放。
dealloc 时 weak 指针自动置 nil 的完整链路
当对象引用计数归零,dealloc 触发,weak 清理链路如下:
dealloc
→ objc_destructInstance
→ objc_clear_deallocating // 清理自身状态 + weak 引用
│
├─ 第 1 步:定位
│ SideTable &table = SideTables[this];
│ lock(table.slock);
│
├─ 第 2 步:从 weak_table 找到自己的 weak_entry_t
│ weak_entry_t *entry = weak_entry_for_referent(&table.weak_table, this);
│ if (!entry) goto done; // 没有被弱引用,直接结束
│
├─ 第 3 步:遍历 entry 的所有 referrer,逐个置 nil
│ for (size_t i = 0; i < entry->num_refs; i++) {
│ id *referrer = entry->referrers[i]; // 拿到 __weak 变量的地址
│ *referrer = nil; // 将那个 __weak 变量置为 nil
│ }
│ // 这就是 dealloc 后所有 weak 变量自动变为 nil 的根本原因
│
├─ 第 4 步:从 weak_table 中移除该 entry
│ weak_entry_remove(&table.weak_table, entry);
│
└─ 第 5 步:清理 refcnts 中的 WEAK 标记,移除该对象的引用计数记录
table.refcnts.erase(this);
关键源码对应:
// objc4 源码精简版
void objc_object::sidetable_clearDeallocating() {
SideTable &table = SideTables()[this];
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
// 存在 weak 引用 → 进入 weak 清理流程
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it); // 从引用计数表移除
}
table.unlock();
}
void weak_clear_no_lock(weak_table_t *weak_table, id referent) {
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) return;
// 收集所有指向该对象的 weak 指针地址
weak_referrer_t *referrers = entry->referrers; // 或 inline_referrers
size_t count = entry->num_refs; // 或 4(inline 情况)
for (size_t i = 0; i < count; ++i) {
id *referrer = referrers[i]; // 这就是某个 __weak 变量的地址
if (*referrer == referent) {
*referrer = nil; // ← 置 nil 的核心操作!
}
}
// 从 weak_table 中删除该 entry
weak_entry_remove(weak_table, entry);
}
完整的数据结构导航图
给定一个 OC 对象 obj,想找到所有指向它的 __weak 变量并置 nil:
obj (对象指针)
│
│ StripedMap::indexForPointer(obj) = ((obj>>4) ^ (obj>>9)) % 8
│
▼
SideTable &table = SideTables[obj] ← 第 0 层:全局 StripedMap
│
│ table.weak_table
▼
weak_table_t weak_table ← 第 1 层:这张 SideTable 下的 weak 表
│
│ weak_entry_for_referent(&weak_table, obj) ← 以 obj 为 key 哈希查找
▼
weak_entry_t *entry ← 第 2 层:该对象的 weak entry
│
│ entry->referrers[i] (或 entry->inline_referrers[i])
▼
id *referrer = entry->referrers[i] ← 第 3 层:某个 __weak 变量自身的地址
│
│ *referrer = nil
▼
该 __weak 变量变为 nil ← 第 4 层(目标)
追问重点 1:weak 表什么时候会清理旧的映射?
objc_storeWeak在设置新值前,会用旧对象的地址去 weak_table 里找到 entry,再从 referrers 数组中移除当前 location。所以 weak 属性重新赋值时,旧引用自动清理。
追问重点 2:为什么 weak 表要在 SideTable 里面而不是全局一张表?同样是为了降低锁粒度——weak 的注册和清理都需要加锁,如果全局一张 weak 表,所有线程的 weak 操作都抢同一把锁,性能灾难。
autoreleasepool 机制
数据结构
autoreleasepool 基于栈结构,以 Page 为单位:
objc_autoreleasePoolPush()
→ AutoreleasePoolPage::push()
→ 在当前线程的 autoreleasepool 栈顶压入一个 POOL_BOUNDARY 哨兵
[obj autorelease]
→ AutoreleasePoolPage::autorelease(obj)
→ 在当前 Page 的 next 位置插入 obj,next++
objc_autoreleasePoolPop(哨兵令牌)
→ AutoreleasePoolPage::pop(哨兵)
→ 从栈顶逐一向后释放对象,直到遇到 POOL_BOUNDARY 哨兵
→ 回滚 next 指针
-
每个线程都有独立的 autoreleasepool 栈(
TLS线程局部存储)。 -
Page 大小 4096 字节(一页内存),Page 之间双向链表连接。
-
Page 满了会创建新 Page(
parent/child指针)。
autoreleasepool 与 RunLoop 的绑定
RunLoop 一次循环:
1. objc_autoreleasePoolPush() ← 压入哨兵
2. 处理事件(Source/Timer/Observer)
3. objc_autoreleasePoolPop(哨兵令牌) ← 回收本次循环产生的临时对象
嵌套场景(手动 @autoreleasepool):
@autoreleasepool { ← Push 哨兵 A
id obj = [NSObject new];
// ...
@autoreleasepool { ← Push 哨兵 B
// ...
} ← Pop 哨兵 B(释放 B 后的临时对象)
// ...
} ← Pop 哨兵 A(释放 A 后的临时对象)
Tagged Pointer
-
对于
NSNumber、NSDate、小NSString等对象,值直接编码在指针中。 -
判断:
最低位为 1(arm64 下0x3标记)。 -
优点:无需 malloc 分配堆内存、无需 retain/release、访问更快。
-
注意:
isEqual:可以比较,==不一定(不同 Tagged Pointer 编码)。
内存泄漏场景排查
4 种常见循环引用链
| 场景 | 原因 | 修复 |
|---|---|---|
| Block 属性 | VC 持有 Block,Block 捕获 self | [weak self] |
| NSTimer | Target 强引用 self,Timer 被 RunLoop 持有 | 系统 API 加到 deinit 里 invalidate,或使用 block-based Timer |
| Delegate | 强引用 delegate(应为 weak) | weak var delegate |
| 闭包网络回调 | VC -> NetworkService -> closure -> VC | [weak self] + guard let self |
deinit 验证
class MyViewController: UIViewController {
deinit {
print("[Leak] \(Self.self) deinit") // 若页面 pop 不打印则泄漏
}
}
高频追问清单
| 问题 | 关键要点 |
|---|---|
| ARC 是编译期还是运行期? | 编译期插桩 + 运行期 SideTable 管理引用计数 |
| weak 的底层数据结构? | SideTable.weak_table.weak_entry_t,内含 referrer 指针数组 |
| dealloc 时 weak 指针何时置 nil? | objc_clear_deallocating -> weak_clear_no_lock -> *referrer = nil |
| autoreleasepool 的释放时机? | 每次 RunLoop 循环结束 Pop,或手动 @autoreleasepool 结束时 |
| Tagged Pointer 的判断方式? | 指针最低有效位标记,arm64 下 objc_taggedPtr 通过掩码判断 |
__weak vs __unsafe_unretained? | weak:自动置 nil;unsafe:野指针 |
| objc_retainAutoreleasedReturnValue 是什么? | 返回值优化(caller/callee 约定,消除多余的 retain/release) |
| 为什么 __weak 变量在使用前要转为 __strong? | 防止多线程环境下对象被释放导致返回 nil |
| SideTable 为什么是 8 张? | 空间换时间,每张独立锁减少竞争;StripedMap 哈希分散对象 |
| refcnts 存的不是真实引用计数? | 存的是 (真实RC-1)<<2,低位被标记位占用;真实RC = 1 + (bits >> 2) |
| weak_entry_t 的 inline 优化? | ≤4 个 referrer 时直接内联存储,无需额外堆分配 |
| extra_rc 和 SideTable refcnts 的关系? | extra_rc 是 isa 内的快速缓存,溢出/不足时才和 SideTable 交互 |
项目落地版
场景 1:批量处理降低内存峰值
// 处理大量图片/payload 时,手动池避免峰值飙升
for (NSInteger i = 0; i < largeArray.count; i++) {
@autoreleasepool {
NSData *data = [self processItem:largeArray[i]];
[writer appendData:data];
}
}
场景 2:循环引用检测器
final class LeakDetector {
static let shared = LeakDetector()
private var livingObjects = NSHashTable<AnyObject>.weakObjects()
func track(_ object: AnyObject) {
livingObjects.add(object)
}
func untrack(_ object: AnyObject) {
livingObjects.remove(object)
}
/// 在页面 pop 后调用,检查是否有 VC 未释放
func checkForLeaks() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self else { return }
for obj in self.livingObjects.allObjects {
print("[LeakDetector] 疑似泄漏: \(type(of: obj))")
}
}
}
}
场景 3:NSTimer 安全封装
final class SafeTimer {
private weak var target: AnyObject?
private let selector: Selector
init(timeInterval: TimeInterval, target: AnyObject, selector: Selector, repeats: Bool) {
self.target = target
self.selector = selector
// 使用 block 避免 target 强持有
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { [weak target] _ in
_ = target?.perform(selector)
}
}
}
学习路径与优先级
初级(P0)— 掌握引用语义和使用边界
-
理解 strong/weak/copy/assign/unsafe_unretained 的区别
-
能解释 ARC 不是 GC
-
掌握 @autoreleasepool {} 基本用法
-
知道循环引用的概念,能识别 Block 中的 [weak self]
-
理解 deinit 和 dealloc 的关系
-
能使用 Instruments -> Leaks / Allocations 排查内存问题
自检:
-
写出 3 种可能产生循环引用的代码模式
-
解释为什么 IBOutlet 用 weak
中级(P0)— 能排查和治理内存问题
-
理解 weak 自动置 nil 的底层实现(SideTable -> weak_table -> weak_entry_t -> referrer)
-
掌握 autoreleasepool Page 的 Page/哨兵/嵌套机制
-
能定位 NSTimer/CADisplayLink 泄漏并修复
-
了解 Tagged Pointer 的工作原理
-
能编写内存泄漏检测工具
-
理解 objc_retainAutoreleasedReturnValue 返回值优化
-
掌握非指针 isa 的 extra_rc 机制及与 SideTable 的交互
-
理解 StripedMap 分片设计和位域存储细节
动手实践:
-
用断点验证 weak 对象在 dealloc 后自动置 nil
-
在 @autoreleasepool 嵌套中打印哨兵地址
-
实现一个简单的内存泄漏检测器
高级(P1)— 建立内存治理体系
-
能设计内存监控和预警体系(水位/峰值/OOM 关联)
-
理解 CF 桥接与 __bridge/__bridge_retained/__bridge_transfer
-
能设计并推动稳定性门禁(内存回归检测)
-
了解 VM Tracker / malloc stack logging 等高级调试工具
-
能为团队沉淀内存治理规范文档
实战项目:
-
为 App 搭建内存水位监控 Dashboard
-
设计循环引用自动检测规则(基于 Malloc Scribble + 堆栈分析)
-
在 CI 中集成 OOM 回归检测