
内存布局

- stack(栈):方法调用、局部变量等
- heap(堆):通过
alloc/new/copy/mutableCopy 等分配的对象
- bss:未初始化的全局变量以及静态变量等
- data:已初始化的全局变量以及静态变量等
- text:程序代码区
内存管理方案
问:iOS 系统怎样对内存进行管理?
根据不同场景,iOS Runtime 会采用不同的策略,常见可归纳为三类:
- Tagged Pointer:例如
NSNumber 等小对象直接编码在指针里
- NONPOINTER_ISA:64 位下
isa 的某些 bit 存储额外信息,并非纯指针
- 散列表(SideTable):引用计数表、
weak 弱引用表(工程中最常遇到的那套)
在学习 Runtime 与内存细节时,可以结合 iOS 相关资料体系化梳理,更容易把各块知识串起来。
NONPOINTER_ISA



散列表方式(SideTable)


struct SideTable
{
spinlock_t slock;// 保证原子操作的自旋锁
RefcountMap refcnts;// 引用计数器存储地,是一个哈希 map
weak_table_t weak_table;// 弱引用表,也是哈希 map 存储
}
问:SideTables 存储在哪?SideTable 存储在哪?
在源码中全局搜索 SideTables,能看到类似 “initialize 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++静态初始化器初始化SideTables,因为libc在我们的C++初始化器运行之前调用我们。
我们也不希望用一个全局的指针指向这个结构体,因为指针是间接访问。
核心信息就是:
- 不能使用 C++ 静态初始化来初始化
SideTables
- 也不希望用一个全局指针指向它(避免额外间接访问)
相关实现如下:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
这里的 StripedMap 是一个模板类型,头部注释一般会说明:
StripedMap< T > is a map of void* -> T
也就是:
StripedMap 是一个 void* 作为 key,T 作为 value 的 map
- 因此 SideTables 本质是全局的 hash 表(按对象地址分流到不同桶/表)
参考链接(原文保留):
问:为什么不是一个 SideTable,而是多个?

如果只有一个 SideTable,所有对象都挤在同一张表里:
- 对任意对象做 retain/release/weak 操作都要对同一把锁加锁
- 结果就是:锁竞争严重,吞吐下降
因此系统采用 分离锁(Striped/分段) 思路:

把一张大表拆成多张表(示意为 8 张)。如果对象 A 落在表 A、对象 B 落在表 B,那么它们的操作可以并发进行,整体效率更好。
问:如何实现快速分流?
题意就是:通过对象指针,如何快速定位到它属于哪张 SideTable。

本质是利用对象地址(指针)做 hash / 掩码运算,映射到固定数量的 stripe(不同 SideTable)。
SideTable 的数据结构
SideTable 主要由三部分构成:
spinlock_t slock:自旋锁,保证原子操作
RefcountMap refcnts:引用计数表(哈希 map)
weak_table_t weak_table:弱引用表(哈希结构)
spinlock_t
自旋锁特点是“忙等”,适用于临界区非常短、加锁开销要极低的场景(当然也要注意长时间持锁会浪费 CPU)。
RefcountMap(引用计数表)

refcnts 是一个哈希 map
- 插入与查找通常走同一套 hash 流程,整体效率高

关键点:
- key:对象地址
- value:
size_t(包含引用计数及其他标志位)
size_t 右移两位,得到引用计数值(低位用于存放标记位)
weak_table_t(弱引用表)

/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
从注释可以读出:
- key:对象 id(可理解为对象地址)
- value:
weak_entry_t
补充一个常见理解点:
weak_entry_t *weak_entries; 表示一个“结构体数组”(数组名 weak_entries,元素类型 weak_entry_t)
一个对象可能被多个 __weak 指针指向,是一对多关系:
__weak NSObject *object = [[NSObject alloc] init];
__weak NSObject *object1 = object;
__weak NSObject *object2 = object;
- key 是
[[NSObject alloc] init] 的地址(也就是 object 里存的地址)
- value 是
weak_entry_t,内部会记录 object/object1/object2 这些弱引用指针的位置(本质上是地址集合)
题外话:我们口头说的 “person 对象” 到底是什么?
YZPerson *person = [[YZPerson alloc] init];
更严谨地说:
person 是指针变量(栈上的变量),里面存的是地址值
- 真正的“对象实例”在堆上,是
[[YZPerson alloc] init] 返回的那块内存
- 所以日常表述里的“person 对象”,多半指的是等号右侧创建出来的实例,而不是指针变量本身
MRC 和 ARC
MRC
手动引用计数相关方法:
alloc
retain
release
retainCount
autorelease
dealloc
注意:在 ARC 下,retain/release/retainCount/autorelease 都禁止显式调用,写了编译器会直接报错。
dealloc 的特殊性
- MRC:需要显式
[super dealloc]
- ARC:允许重写
dealloc 来释放非 OC 资源(移除通知、释放 C 指针等),但禁止在内部调用 [super dealloc](编译器会自动处理)
autorelease 的替代方案
ARC 虽然禁止 [obj autorelease],但 autorelease 机制仍存在,主要通过:
@autoreleasepool 代码块
__autoreleasing 修饰符
- 编译器对方法命名规则(Naming Conventions)的所有权推导
MRC 中 autorelease 很常用,这点要特别留意。
ARC
ARC 是 LLVM(编译期)与 Runtime(运行期)协作的结果:
- LLVM(编译期):静态分析代码,在合适位置插入
objc_retain/objc_release
- Runtime(运行期):处理
weak(置 nil)与 autorelease 的优化细节
ARC 下的规则要点:
- 禁止调用
retain/release/retainCount/dealloc
- 允许重写
dealloc,但禁止 [super dealloc]
- 新增
weak/strong 等属性关键字
引用计数管理
下面按 alloc/retain/release/retainCount/dealloc 的路径梳理一次实现逻辑。
alloc 的实现
alloc 最终会走到 C 的 calloc,分配并清零内存;关键点是:并不会在此处把引用计数“显式设置为 1”。
+ (id)alloc {
return _objc_rootAlloc(self);
}
进入_objc_rootAlloc(self)
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
进入callAlloc
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());//C语言的calloc函数
if (!obj) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
obj->initInstanceIsa(cls, dtor);里面有
initIsa(cls, true, hasCxxDtor);
进入initIsa
inline void
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!indexed) {
isa.cls = cls;
} else {
assert(!DisableIndexedIsa);
isa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.indexed is part of ISA_MAGIC_VALUE
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
完成
在里面没有发现关于sidatable表的操作
这点容易误解:很多人长期以为 alloc 会“+1”,但从这条路径看,并没有直接操作 SideTable。
retain 的实现

- (id)retain {
return _objc_rootRetain(self);
}
进入
NEVER_INLINE id
_objc_rootRetain(id obj)
{
ASSERT(obj);
return obj->rootRetain();
}
进入
inline id
objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
进入
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
//#define SIDE_TABLE_RC_ONE (1UL<<2)左移两位,是4(实际反应出来是1)
}
table.unlock();
return (id)this;
}
三句核心操作:
SideTable& table = SideTables()[this];:定位对象所属 SideTable
size_t& refcntStorage = table.refcnts[this];:获取该对象的 size_t 存储单元
refcntStorage += SIDE_TABLE_RC_ONE;:引用计数 +1(以位移形式存放)
问:retain 时系统如何查找引用计数?
可以理解为“两次 hash 查找”:
- 从
SideTables 通过对象指针定位到对应 SideTable
- 在
SideTable.refcnts 中再通过对象指针定位到 size_t 存储位
最终表现就是引用计数加一。
release 的实现

- (oneway void)release {
((id)self)->rootRelease();
}
进入
ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
进入
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
......
return sidetable_release(performDealloc);
}
进入sidetable_release
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (table.trylock()) {
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
对应三句关键点:
SideTable& table = SideTables()[this];:找到 SideTable
RefcountMap::iterator it = table.refcnts.find(this);:找到引用计数条目
it->second -= SIDE_TABLE_RC_ONE;:引用计数 -1(未 pinned 时)
retainCount 的实现

- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
进入rootRetainCount()
inline uintptr_t
objc_object::rootRetainCount()
{
assert(!UseGC);
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
if (bits.indexed) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
进入sidetable_retainCount()
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
关键步骤:
SideTable& table = SideTables()[this];:定位 SideTable
size_t refcnt_result = 1;:先把返回值初始化为 1
it = table.refcnts.find(this);:查找引用计数记录
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;:把 size_t 右移得到的计数累加回来
return refcnt_result;:返回结果
问:新创建对象,alloc 后引用计数是多少?
按这里的口径:
- 新建对象“额外计数”为 0
- 调用
retainCount 时,内部以 refcnt_result = 1 起步,最终返回 1
也就是说:alloc 后 retainCount 的观测值通常为 1,但其底层计数存储并不等同于“alloc 时写入 1”。
dealloc 的实现与副作用

object_dispose() 实现示意:

objc_destructInstance() 与 clearDeallocating() 相关:


从这些流程可以看到:
dealloc 期间会触发 弱引用置 nil
dealloc 期间会对 引用计数表记录进行清理/擦除
问:关联对象是否需要在 dealloc 手动移除?
通过关联对象给类“加实例变量”时,一般不需要在 dealloc 里手动移除,因为在对象销毁路径中 Runtime 已包含关联对象的清理操作。
弱引用管理(weak)

理解要点:
__weak 并不是“特殊指针类型”,而是一种语义:编译器会把相关操作落到 objc_initWeak 等函数上
- 类似地,
strong 的语义会对应到 retain/release 路径(可理解为 objc_initStrong 一类处理)

id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
进入storeWeak
static id
storeWeak(id *location, objc_object *newObj)
{
if (HaveNew) {
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
(id)newObj, location,
CrashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
}

- 如果已存在对应
weak_entry_t,就把当前弱引用位置插入进去
- 如果不存在,则新建一个
weak_entry_t 并插入

问:weak 变量自动变 nil 是如何实现的?
对象销毁时会走清理逻辑:
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}

结论:
当对象 dealloc 后,内部会调用弱引用清理相关函数。该函数会根据对象指针定位到弱引用表,把对应的弱引用位置集合取出(数组/集合),再逐个遍历,将这些 weak 指针 全部置为 nil。
自动释放池(AutoreleasePool)

问:array 在什么时候释放?(自动释放池与 dealloc 的关系)
在一次 RunLoop 即将结束时,会执行 AutoreleasePool::pop(),对池内对象统一 release:
- 自动释放池
pop
- 对象引用计数到 0
- 触发
dealloc
- 对象真正释放
注意:先 release 使引用计数归零,才会进入 dealloc。不是先 dealloc 再把计数设置为 0。
⚠️:以上对工厂方法(例如 [NSMutableArray array])更典型;对 [[NSArray alloc] init] 这种情况不完全相同。
不是所有 array 都会等到 RunLoop 结束
在 ARC 下常见分两类:
情况 A:alloc/init 创建的局部变量(大多数场景)
- (void)test {
NSArray *array = [[NSArray alloc] init];
// ... 使用 array
} // 函数结束
- 关键点:这种
array 多数情况下不会进入自动释放池
- 释放时机:编译器能分析出变量作用域,会在
} 前插入 objc_release(array)
- 结果:作用域结束时立即
dealloc,无需等待 RunLoop
这是 ARC 的常见性能优化:减少 autorelease 的开销、降低内存峰值。
情况 B:工厂方法创建(且未被优化)
- (void)test {
NSArray *array = [NSArray array]; // 传统的 autorelease 对象
}
- 这种通常会进入 AutoreleasePool
- 一般在 RunLoop 即将休眠(BeforeWaiting)或退出(Exit)时,pool
pop 才释放
总结成一句:alloc/init 的局部变量通常直接 release;工厂方法对象通常延迟到 pool pop。
官方文档的“方法家族”(Method Families)
ARC 编译器会根据方法名推导所有权:
-
Own-returning methods:alloc/new/copy/mutableCopy 开头
- 调用者默认拥有所有权(+1)
- 编译器会在作用域结束插入
release(-1),不依赖自动释放池
-
Non-own-returning methods:例如 [NSArray array] 等
- 调用者不拥有所有权
- 为保证返回对象不过早销毁,通常通过 AutoreleasePool 或 TLS 优化延长寿命
这个差异对 内存峰值优化很关键:例如循环内大量临时对象,如果是工厂方法创建更容易“堆到 RunLoop 才释放”,就需要考虑 @autoreleasepool。
特殊情况补充:weak 可能延长对象寿命
严格来说,“局部变量 alloc/init 不进池子”虽然很常见,但存在更复杂的特例,例如引入 __weak 指针时:
- (void)test {
NSArray *arr = [[NSArray alloc] init];
__weak NSArray *weakArr = arr; // 引入了 weak
}
此时编译器可能出于语义正确性与时序安全考虑,采用不同的延长寿命策略(包括可能走 autorelease 路径),避免在 weak 读写的中间窗口出现不可控状态。
题外话:数组变量销毁 vs 对象释放
当你问“array 什么时候释放”时,需要区分:
array 这个指针变量本身:在栈上,作用域结束时变量消失
array 指向的对象:在堆上,真正释放取决于引用计数与释放路径

因此更准确的问法是:array 指向的对象什么时候释放。
问:AutoreleasePool 为何可以嵌套使用?
多层嵌套的本质,是在池结构中多次插入“哨兵对象”(pool boundary),以便 pop 时分段回收。
问:AutoreleasePool 的实现原理是怎样的?



“批量操作”指每次 pop 会把对应边界内的对象统一 release。
自动释放池的结构特点
- 以“栈”式入栈/出栈为核心语义
- 内部通过双向链表组织多页(page)
- 与线程是一一对应关系

AutoreleasePoolPage

class AutoreleasePoolPage
{
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
}


[obj autorelease] 的实现

核心调用链(按原文保留):
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
rootAutorelease2() 最终会走到:
AutoreleasePoolPage::autorelease((id)this)
autoreleaseFast(obj)
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {// page 有值且没满
return page->add(obj);
} else if (page) {// page 有值但满了
return autoreleaseFullPage(obj, page);
} else {// page 为空
return autoreleaseNoPage(obj);
}
}
autoreleaseFullPage(obj, page):
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
if (page->child)
page = page->child;
else
page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
含义概括:
- hot page 满了:优先找
child
- 没有
child:新建 AutoreleasePoolPage
- 找到非满页后
add(obj)
autoreleaseNoPage(obj):
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
assert(!hotPage());
if (obj != POOL_SENTINEL && DebugMissingPools) {
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested.
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
// Push the requested object.
return page->add(obj);
}
含义概括:
- 当前线程没有 page:先新建一页
- 需要的话先插入
POOL_SENTINEL
- 再把对象
add 进去
最终小结(按原文逻辑整理):
- page 存在且没满:直接插入
obj
- page 存在但满了:找子 page,没有就新建,再插入
obj
- page 不存在:新建 page(并处理哨兵),再插入
obj


循环引用
循环引用常见分三种:
自循环引用

示例场景:
YZPerson 内有成员变量 YZStudent *student
- 例如
YZStudent *student = [[YZPerson alloc] init]; 这类引用链若形成闭环就会泄漏


工程中更常踩坑的点包括:
- 代理(delegate)
- block
NSTimer
- “大环引用”(多对象交错持有)
下面重点看 NSTimer。
问:如何破除循环引用?
两个方向:
- 从源头避免闭环(设计上不形成强引用环)
- 在合适时机主动断环(手动置空/销毁/解除绑定)
问:具体方案有哪些?
__weak
__block
__unsafe_unretained

__block 破解循环引用
- MRC:
__block 修饰对象不会增加引用计数,能在一定程度上避免循环引用
- ARC:
__block 修饰对象默认会被强引用,通常无法避免循环引用,需要手动解环

__unsafe_unretained 破解循环引用
- 修饰对象不会增加引用计数,能避免某些循环引用
- 但对象释放后可能产生悬垂指针(野指针)风险更高,因此一般不建议优先使用
__unsafe_unretained
NSTimer 的循环引用与 RunLoop 的强引用

典型链路:
- VC 强引用某对象
- 对象强引用
NSTimer
NSTimer 强引用 target(对象)
- 形成环,导致无法释放
能否把“对象持有 NSTimer”改成弱引用解决?

这里还要注意另一条强引用链:
NSTimer 加入当前线程的 RunLoop
- RunLoop 会对
NSTimer 强引用
- 如果是主线程,主 RunLoop 强持有 timer;timer 又引用对象
即使 VC -> 对象 的强引用断开,对象仍可能因为 timer/RunLoop 链路存活,造成泄漏
另外:
- 非重复定时器:通常在回调里销毁并置
nil,可以解除循环引用并释放
- 重复定时器:不能简单在回调里销毁(逻辑上往往不成立),需要更稳妥的解法
原文提到的“中间变量解除循环引用”的方式不如某些成熟实现(如尝试让 NSTimer 对对象弱引用)更优,这里保持原意不展开。
延伸阅读与知识体系建议
从 SideTable、weak、AutoreleasePool 到循环引用,这些内容都属于 Runtime + 内存管理核心链路。进一步想把“锁竞争、哈希分流、RunLoop 生命周期、对象销毁路径”串起来时,可以参考 网络/系统 中的并发与基础系统知识来补足底层视角。