前言
前面我们已经学习了解了sideTable,今天来看看在OC中,sideTable是如何在我们使用weak时工作的。在OC中,weak修饰符是一种用于声明"弱引用"的关键字,其核心特性是不参与对象的引用计数管理,而且当被引用的对象被释放时,weak指针会自动置为nil(避免野指针)。为探究weak的工作原理和底层逻辑,笔者特写此篇来记录对weak的学习。
引用计数与sideTable
OC的内存管理是基于引用计数的(ARC 下由编译器自动生成引用计数增减代码)。对象的isa指针指向其类信息,而对象的内存分布中通常包含:
- isa指针:指向对象的所属类或元类。
- 引用计数相关数据:早期直接存储在对象内存头部,后来为了减少内存碎片,现将引用计数和弱引用信息分离到sideTable中。
sideTable是什么
sideTable是一个辅助数据结构(本质是哈希表),用于存储与对象关联的元数据,包括引用计数表、弱引用表和其它元数据(如关联对象、关联引用等)。
引用计数表(RefcountMap):记录对象的引用计数值(分散存储,避免每个对象都占用额外空间)。
弱引用表(WeakMap):记录所有指向该对象的 weak指针地址(键为对象地址,值为 weak指针的集合)。
没个对象可能共享一个sideTable(通过哈希计算映射),因此sideTable的内存开销备份谈到多个对象上。
weak修饰符的底层实现原理
首先,我们来看看调用weak时的底层调用。
weak工作流程第二阶段
我们在调用weak的地方打上断点,然后进行汇编代码调试,然后我们能发现,weak调用了一个objc_storeWeak方法:
然后我们在obj4-906-main源码中找到了这部分方法的底层实现:

这其实是weak的赋值阶段,用于将weak指针的地址注册到目标对象的弱引用表中。
这个函数中的参数含义如下:
- location:指向 weak变量的指针(即存储 weak指针的内存地址)。例如,有一个 __weak NSObject *obj;,则 &obj就是 location的值。
- newObj:要赋值给 weak变量的新对象(可能为 nil)。
- 返回值:返回旧值(即 location原来的对象,若未修改则为 nil)。
返回值中,有三个参数控制 storeWeak函数的行为逻辑:
DoHaveOld:是否处理旧值
若为 true,函数会先检查 location原有的旧值(即之前指向的对象),并从该旧值的弱引用表中移除当前 location地址(避免旧对象释放时错误地清理已失效的 weak指针);
若为 false,则跳过旧值处理(适用于首次赋值或无需清理旧值的场景)。
DoHaveNew:是否处理新值
若为 true,函数会将 newObj的地址注册到其对应的弱引用表中(即把location地址添加到 newObj的弱引用表 referrers数组中);
若为 false,则跳过新值处理(适用于清空 weak指针的场景,如 obj = nil)。
DoCrashIfDeallocating:对象释放时是否崩溃
若为 true,当 newObj正在被释放(deallocating状态)时,函数会触发崩溃(避免向已释放对象注册弱引用);
若为 false,则允许向正在释放的对象注册弱引用(但后续对象释放时会清理该 weak指针)。
storeWeak函数源码:
objective-c
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
weak工作流程第一阶段
刚刚说objc_storeWeak方法是weak的赋值和持有阶段,这是调用weak的第二阶段,在这之前还有一个声明阶段即初始化阶段。
初始化时,runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
源码如下:

weak工作流程第三阶段
weak工作流程的第三阶段就是对象释放阶段(清楚弱引用表并置nil):

objc_object::clearDeallocating()函数,它是一个内联函数,内部直接调用了sidetable_clearDeallocating()。这说明clearDeallocating()是对外暴露的接口,而sidetable_clearDeallocating()是具体的实现细节,负责实际的清理操作。

这个函数的主要操作是处理SideTable中的弱引用表和相关元数据。步骤包括:
- 获取SideTable:通过SideTables()[this]获取当前对象对应的SideTable实例。
- 加锁:使用table.lock()确保线程安全,避免多线程同时修改SideTable导致数据竞争。
- 查找引用计数项:在table.refcnts(引用计数表)中查找当前对象的迭代器it。
- 检查弱引用标记:如果找到迭代器且其值包含SIDE_TABLE_WEAKLY_REFERENCED标志(表示该对象有弱引用需要处理),则调用weak_clear_no_lock函数清理弱引用表。
- 清理引用计数项:从refcnts中删除当前对象的条目。
- 解锁:释放SideTable的锁,确保其他线程可以继续操作。
weak的本质
- 运行时维护的弱引用跟踪机制
weak的本质是运行时通过SideTable动态跟踪对象与weak指针的关联关系。其核心特性(不参与引用计数、自动置nil)均由以下机制支撑:
- 不参与引用计数:weak赋值时不调用retain,对象释放时不依赖weak指针的计数。
- 自动置nil:通过SideTable中的弱引用表,在对象释放时主动遍历并清理所有关联的weak指针地址。
- 弱引用表的集中管理
Weak(即__weak修饰的指针)的本质是运行时在SideTable中维护的一张弱引用表(weak_table_t)。该表存储了所有指向当前对象的weak指针地址(referrers数组),是对象释放时定位并清理weak指针的核心依据。
weak置nil
weak指针置nil的关键操作发生在对象释放阶段,由sidetable_clearDeallocating
函数触发:
- 对象调用
dealloc
后,执行objc_object::clearDeallocating
(内联函数),调用sidetable_clearDeallocating
。 sidetable_clearDeallocating
获取对象的SideTable并加锁,检查引用计数映射(refcnts)中是否存在SIDE_TABLE_WEAKLY_REFERENCED
标记(表示被weak引用过)。- 若存在,调用
weak_clear_no_lock
函数,遍历该对象的弱引用表(weak_table_t.referrers
),将每个weak指针的地址(entry->referrers)处的值置为nil(本质是修改内存为0)。 - 清理完成后,删除引用计数映射项并解锁SideTable,完成
weak
指针的置nil操作。
总结
weak的实现以SideTable和弱引用表为核心,通过运行时动态跟踪对象与weak指针的关联关系,在对象释放时主动清理所有weak指针并置nil,既避免了循环引用导致的内存泄漏,又保证了内存访问的安全性,其本质是运行时维护的弱引用跟踪机制。