二.iOS内存管理

107 阅读14分钟

内存管理(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
对比项MRCARC
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_REFERENCEDDEALLOCATING 两个标记位占用,引用计数只能从 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)    │magicdeallocweakly_refhas_assocextra_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


objStripedMap::indexForPointer(obj) // 哈希取模SideTables.tables[i] // 拿到对应 SideTabletable->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)
        → 在当前 Pagenext 位置插入 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

  • 对于 NSNumberNSDate、小 NSString 等对象,值直接编码在指针中。

  • 判断:最低位为 1(arm64 下 0x3 标记)。

  • 优点:无需 malloc 分配堆内存、无需 retain/release、访问更快。

  • 注意:isEqual: 可以比较,== 不一定(不同 Tagged Pointer 编码)。


内存泄漏场景排查

4 种常见循环引用链
场景原因修复
Block 属性VC 持有 Block,Block 捕获 self[weak self]
NSTimerTarget 强引用 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_unretainedweak:自动置 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 分片设计和位域存储细节

动手实践

  1. 用断点验证 weak 对象在 dealloc 后自动置 nil

  2. 在 @autoreleasepool 嵌套中打印哨兵地址

  3. 实现一个简单的内存泄漏检测器

高级(P1)— 建立内存治理体系

  • 能设计内存监控和预警体系(水位/峰值/OOM 关联)

  • 理解 CF 桥接与 __bridge/__bridge_retained/__bridge_transfer

  • 能设计并推动稳定性门禁(内存回归检测)

  • 了解 VM Tracker / malloc stack logging 等高级调试工具

  • 能为团队沉淀内存治理规范文档

实战项目

  1. 为 App 搭建内存水位监控 Dashboard

  2. 设计循环引用自动检测规则(基于 Malloc Scribble + 堆栈分析)

  3. 在 CI 中集成 OOM 回归检测