本文将对iOS的内存管理机制进行一次深入的底层探索,涵盖从基础的内存布局到复杂的SideTables、自动释放池实现原理,并详细分析循环引用的成因与解决方案。

内存布局
程序的内存通常被划分为以下几个区域:

- 栈(Stack):由编译器自动分配释放,存放函数的参数值、局部变量值等。方法调用就在此区域进行。
- 堆(Heap):由程序员分配和释放,通过
alloc、new 等操作创建的对象都存放在这里。
- BSS段:存放未初始化的全局变量和静态变量。
- 数据段(Data):存放已初始化的全局变量和静态变量。
- 代码段(Text):存放程序的可执行代码。
理解程序的基础网络/系统内存布局是分析高级内存管理机制的前提。
内存管理方案
iOS系统针对不同场景,提供了多样化的内存管理方案,主要有以下三种:
- Tagged Pointer:用于存储如NSNumber等小对象,将数据直接存放在指针地址中。
- NONPOINTER_ISA:在64位架构下,
isa指针的部分比特位被用于存储引用计数等额外信息,而不仅仅是指向类对象的地址。
- 散列表(SideTables):这是我们通常讨论的引用计数和弱引用管理机制的核心,包含引用计数表和弱引用表。
NONPOINTER_ISA



NONPOINTER_ISA 利用64位指针的富裕空间,在 isa 中内嵌了对象的引用计数(extra_rc)、是否拥有弱引用等信息,这是对内存和性能的一种优化。
散列表(SideTables)方式
当 NONPOINTER_ISA 的存储空间不足以存放引用计数时,或者需要管理弱引用时,系统就会使用散列表。


其核心数据结构是 SideTable:
struct SideTable
{
spinlock_t slock; // 保证原子操作的自旋锁
RefcountMap refcnts; // 引用计数哈希表
weak_table_t weak_table; // 弱引用哈希表
}
SideTables的存储与本质
在Runtime源码中,SideTables 是通过一个静态函数返回的。

// We cannot use a C++ static initializer to initialize SideTables because
// libc calls us before our C++ initializers run. We also don‘t want a global
// pointer to this struct because of the extra indirection.
// Do it the hard way.
注释说明,由于初始化时机问题,不能使用C++静态初始化器,也没有使用全局指针。
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
StripedMap 是一个以 void* 为键(Key),模板类型 T(这里是 SideTable)为值(Value)的映射表。因此,SideTables() 本质上是一个全局的哈希表。
为什么需要多个SideTable?(分离锁技术)
如果全局只有一个 SideTable,那么任何线程对任何对象进行引用计数操作时,都需要对这张大表加锁,这会引发严重的效率问题。

为了解决这个问题,系统采用了 分离锁 技术。将一张大表拆分成多个(例如64个)SideTable。这样,对不同 SideTable 中对象的操作就可以并行进行,大大提升了并发性能。

如何快速定位对象所属的SideTable?
系统通过哈希函数,将对象的内存地址映射到固定的 SideTable 索引上,实现快速分流。

SideTable的数据结构详解
回顾 SideTable 的三个成员:
spinlock_t slock:自旋锁,适用于轻量、快速的访问场景。
RefcountMap refcnts:引用计数表。
weak_table_t weak_table:弱引用表。
RefcountMap 引用计数表
这是一个哈希表,以对象的 DisguisedPtr(伪装的对象地址)为键,以 size_t 为值。这个 size_t 的值在位移后(通常是右移2位)才代表对象实际的引用计数。


weak_table_t 弱引用表
同样是一个哈希表结构,它以对象地址为键,以 weak_entry_t 结构体为值。一个对象可能被多个 __weak 变量指向,这种一对多的关系就存储在 weak_entry_t 中。

struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
MRC 与 ARC
MRC(手动引用计数)
开发者需要手动调用 retain, release, autorelease, retainCount, dealloc 等方法管理内存。在MRC中,必须在 dealloc 方法中显式调用 [super dealloc]。
ARC(自动引用计数)
由编译器(LLVM)和运行时(Runtime)协作完成。编译器在编译期插入合适的 retain 和 release 调用;运行时则负责管理弱引用和自动释放池等。ARC下禁止显式调用 retain, release, retainCount, dealloc。可以重写 dealloc 来释放非Objective-C资源(如CF对象、移除通知),但禁止调用 [super dealloc](编译器自动处理)。ARC引入了 __weak 和 __strong 等所有权修饰符。
引用计数管理实现剖析
alloc 实现
alloc 方法的核心是调用 C 函数 calloc 分配内存,并初始化 isa 指针。值得注意的是,在 alloc 阶段,对象的引用计数并未被设置为1,这与许多人的直觉相悖。
retain 实现
retain 操作最终会走到 sidetable_retain 函数。

id objc_object::sidetable_retain() {
SideTable& table = SideTables()[this]; // 1. 通过哈希找到SideTable
table.lock();
size_t& refcntStorage = table.refcnts[this]; // 2. 找到引用计数存储位置
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE; // 3. 引用计数+1 (实际加4)
}
table.unlock();
return (id)this;
}
流程:通过对象地址两次哈希查找(先找 SideTable,再在 RefcountMap 中找具体条目),然后增加引用计数。
release 实现
release 操作的核心是减少引用计数,并在计数为0时触发 dealloc。

uintptr_t objc_object::sidetable_release(bool performDealloc) {
SideTable& table = SideTables()[this]; // 1. 找到SideTable
bool do_dealloc = false;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this); // 2. 找到条目
if (it == table.refcnts.end()) { // 未找到,说明计数为0
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) { // 计数小于阈值,将变为0
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE; // 3. 引用计数-1
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
retainCount 实现
retainCount 的返回值是 1 + extra_rc + sidetable中的计数。对于新 alloc 的对象,其散列表中的计数为0,但 retainCount 方法会返回1。

dealloc 实现
dealloc 的内部调用链非常清晰,负责对象的最终销毁工作。

调用链:dealloc -> _objc_rootDealloc -> rootDealloc

关键函数 object_dispose 负责处理销毁实例。

objc_destructInstance 依次处理C++析构函数和关联对象。
总结 dealloc 的关键操作:
- 调用 C++ 析构函数(如果对象有
hasCxxDtor)。
- 移除关联对象(
_object_remove_associations)。
- 清理弱引用表和引用计数表。

最后一步:sidetable_clearDeallocating 清除弱引用并擦除引用计数条目。
关联对象是否需要手动移除?
不需要。系统在 dealloc 时已自动调用 _object_remove_associations() 进行清理。
弱引用(__weak)管理机制
弱引用的建立
__weak 修饰符在编译时会被转换为对 objc_initWeak 函数的调用。

其核心流程是调用 storeWeak,并最终通过 weak_register_no_lock 将弱引用指针注册到对应对象的 weak_entry_t 中。


弱引用的清除(自动置nil)
当对象被释放时,在 dealloc 的清理阶段,会调用 weak_clear_no_lock 函数。

该函数从弱引用表中取出该对象对应的所有弱引用指针数组,然后遍历这个数组,将其中每一个弱引用指针的值都置为 nil。这正是弱引用“自动置nil”功能的实现原理。
自动释放池(AutoreleasePool)
自动释放池的作用与时机
自动释放池的主要作用是延迟对象的 release 时机。对于工厂方法(如 [NSArray array])返回的对象,系统会将其加入自动释放池,通常在当前 RunLoop 迭代结束时进行释放。

重要区分:
alloc/init 创建的局部变量:编译器(遵循方法家族命名约定)通常会在作用域结束时直接插入 release,不会加入自动释放池。这是ARC的性能优化。
- 工厂方法创建的对象:通常会被加入自动释放池,在RunLoop休眠或池子
pop 时释放。
自动释放池的实现原理
@autoreleasepool {} 在编译时会被改写为 push 和 pop 调用。

void *ctx = objc_autoreleasePoolPush();
// {} 中的代码
objc_autoreleasePoolPop(ctx);


自动释放池底层是由一系列 AutoreleasePoolPage 以双向链表形式连接而成的栈结构,且与线程一一对应。

AutoreleasePoolPage 结构
class AutoreleasePoolPage {
id *next; // 指向下一个可存放autorelease对象地址的位置
pthread_t const thread; // 所属线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
// ...
};

每个 Page 就像一页栈。push 操作会插入一个哨兵对象(POOL_SENTINEL),pop 操作时会释放从上次 push 之后直到这个哨兵对象之间所有加入池中的对象。

autorelease 方法实现
[obj autorelease] 方法的本质,就是调用 autoreleaseFast 函数,将对象加入当前线程的 hotPage(当前活跃的Page)。

流程简述:
- 如果当前
hotPage 存在且未满,直接加入。
- 如果
hotPage 已满,则沿着 child 指针查找或创建新的Page,并加入。
- 如果没有
hotPage,则创建第一个Page,先加入哨兵对象,再加入目标对象。
pop 操作
pop 操作根据传入的哨兵对象上下文,找到对应位置,然后向该位置之前的所有对象发送 release 消息,最后回滚 next 指针。

嵌套的 @autoreleasepool 就是多次插入哨兵对象,pop 时则一层层释放。在如 for 循环内创建大量临时对象的场景,手动添加 @autoreleasepool 可以及时降低内存峰值。
循环引用及解决方案
循环引用主要发生在对象间相互强引用,导致引用计数无法降为零的情况。常见于Delegate、Block、NSTimer等场景。
循环引用的类型
- 自循环引用:对象强持有自身。
- 相互循环引用:两个对象相互强引用。
- 多循环引用:多个对象形成环状强引用链。

解决方案
核心思路:避免产生循环引用 或 在合适时机手动断环。
常用修饰符:
__weak:不会增加引用计数,被修饰对象释放后指针自动置nil。最安全常用。
__unsafe_unretained:不会增加引用计数,但对象释放后会产生悬垂指针,不安全。
__block(在MRC下):可以用于打破Block的循环引用(因为MRC下__block不会retain对象)。在ARC下,__block变量会被强引用,通常需要配合在Block内手动置nil来断环。
NSTimer的循环引用问题
NSTimer会强引用它的 target,如果 target(例如ViewController)又强引用这个Timer,就会形成循环引用。此外,RunLoop也会强引用已加入的Timer,这加剧了问题的复杂性。

解决方案通常采用“中间对象”模式,让一个弱引用 target 的中间类来充当Timer的 target,并在中间类的方法中回调真正的目标。或者使用iOS 10+的 block 形式API。深入理解iOS内存管理的这些底层机制,不仅能帮助我们编写更健壮、高效的代码,也是应对复杂内存问题、进行性能调优的坚实基础。对于希望深入Android/iOS底层开发的工程师来说,这是必修课。