文章目录
前言
在 iOS/macOS 的内存管理中,Side Table(边表) 是苹果为优化对象元数据存储而设计的 辅助数据结构。它与对象的 isa 指针紧密关联,用于存储对象的额外信息(如引用计数、弱引用指针、关联对象等),解决了传统对象内存布局无法高效扩展的问题。它的设计与使用是OC运行时实现弱引用的基础,使得ARC能够正确地处理弱引用的生命周期。
1️⃣Side Table 的核心作用:扩展对象元数据存储
1.1 传统对象的内存限制
OC 对象的内存布局以 isa 指针为核心(指向类对象),但仅靠 isa 无法存储所有元数据:
- 普通对象的 isa 指针仅指向类对象,无法直接存储引用计数、弱引用等动态信息。
- 若为每个对象单独分配元数据内存,会导致内存碎片和性能下降。
1.2 Side Table 的定位:集中式元数据仓库
Side Table 是 全局共享的哈希表(或数组),为需要额外元数据的对象提供统一的存储空间。它的核心作用是:
- 存储对象的扩展元数据(如引用计数、弱引用指针、关联对象键值对)。
- 减少内存碎片:多个对象共享同一 Side Table 的存储空间,避免为每个对象单独分配内存。
2️⃣Side Table 的底层结构与关联
2.1 Side Table 与 isa 指针的关系
OC 对象的 isa 指针(Class 类型)在 64 位系统中被扩展为 uintptr_t
类型(无符号长整型),通过 位域(Bit Field) 存储额外信息:
- 低 48 位:指向类对象或 Side Table 的地址(具体取决于对象类型)。
- 高 16 位:存储标记位(如是否为 weak 对象、是否需要 Side Table 等)。
/*_arm64(对应ios移动端)*/
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; /*是否使用非指针格式*/ \
uintptr_t has_assoc : 1; /*是否有关联对象*/ \
uintptr_t has_cxx_dtor : 1; /*是否有C++析构函数*/ \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ /*存储类指针的核心字段(加密后)*/ \
uintptr_t magic : 6; /*验证指针合法性 调试器将提取的 magic_value 与预定义的合法值(ISA_MAGIC_VALUE)比较*/ \
uintptr_t weakly_referenced : 1; /*是否为弱饮用*/ \
uintptr_t unused : 1; /*保留位(数据结构或协议中预留的未使用位)*/ \
uintptr_t has_sidetable_rc : 1; /*是否使用弱引用表*/ \
uintptr_t extra_rc : 19 /*内联引用计数(最大2^19-1)*/
# elif __x86_64__ //__x86_64__(对应macos)(与上面一样)
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8 /*(最大为2^8-1)*/
在上述isa的结构体代码中,extra_rc
和 has_sidetable_rc
,这两者共同记录引用计数器。
关键逻辑:
当对象需要存储额外元数据时(如弱引用),isa 指针的高位会被标记为“需要 Side Table”,并通过低 48 位指向对应的 Side Table 条目。
2.2 Side Table 的存储结构
Side Table 的底层实现是一个 全局的哈希表(或数组),每个条目对应一个对象的内存地址,存储其扩展元数据。不同平台(iOS/macOS)的实现略有差异,但核心结构相似:
字段 | 描述 |
---|---|
refcount |
引用计数(用于 ARC 管理)。 |
weak_refs |
弱引用指针数组(存储指向该对象的弱引用变量地址)。 |
associated_objects |
关联对象键值对(存储通过 objc_setAssociatedObject 设置的关联数据)。 |
flags |
标记位(如是否被标记为待释放、是否为类对象等)。 |
在obj4-906中,sideTable部分的结构如下:
struct SideTable {
spinlock_t slock; //自旋锁
RefcountMap refcnts; //引用计数映射表
weak_table_t weak_table; //弱引用表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
} //SideTable():构造函数,使用memeset将weak_table初始化为0,确保弱引用表的初始状态安全。
~SideTable() {
_objc_fatal("Do not delete SideTable.");
} //~SideTable():析构函数,调用 _objc_fatal("Do not delete SideTable.")防止手动释放。SideTable的生命周期由 Objective-C 运行时管理(如对象销毁时自动释放),禁止用户主动删除。
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
//直接操作自旋锁 slock,用于保护对 refcnts和 weak_table的修改。
void reset() { slock.reset(); } //重置自旋锁状态(具体实现依赖 spinlock_t的底层逻辑,通常用于释放锁或重置为未锁定状态)。
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
slock是一个自旋锁:保证对 SideTable中数据(如引用计数、弱引用表)的线程安全访问。自旋锁适用于临界区较小的场景(引用计数修改通常很快),避免线程阻塞开销。
refcnts是引用计数映射表:存储当前对象的引用计数值。RefcountMap本质是一个轻量级的哈希表或键值对结构,键为对象的内存地址(或唯一标识),值为对应的引用计数值。
weak_table是弱引用表:存储所有指向当前对象的弱引用指针(__weak修饰的指针)。弱引用表的核心功能是:当对象被释放时,自动将所有弱引用置为 nil,避免野指针。
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; //弱引用条目数组:指向 weak_entry_t结构体的指针数组。每个 weak_entry_t对应一个指向当前对象的 __weak指针,存储弱引用的具体信息(如指针地址、对象状态等)
size_t num_entries; //弱引用条目数量:记录当前 weak_table_t中有效弱引用条目的数量(即 weak_entries数组中实际使用的元素个数)
uintptr_t mask; //哈希掩码:用于计算弱引用的哈希索引,优化哈希表的存储和查找效率
//弱引用的哈希值通常通过 hash ^ mask计算(hash是弱引用指针的哈希值),结果对齐到 weak_entries数组的大小。mask的值通常为数组大小减一(如数组大小为 2^n,则 mask = (1 << n) - 1),确保索引在数组范围内
uintptr_t max_hash_displacement; //最大哈希位移
};
关于weak_entry_t,,其源码在obj4_906中是这样的:
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers; // 外联弱引用指针数组(动态分配)
uintptr_t out_of_line_ness : 2; // 标记为外联存储(固定为 1)
uintptr_t num_refs : PTR_MINUS_2; // 弱引用数量(减去 2 位用于其他标记)
uintptr_t mask; // 哈希掩码(用于快速查找)
uintptr_t max_hash_displacement; // 最大哈希位移(解决哈希冲突)
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
2.3 SideTable 的工作流程
由上述源码,我们可以略微推断出sideTable的工作流程:
- 对象创建:当对象大小超过小对象阈值时,运行时为其分配
SideTable
,并将引用计数初始化为 1(retainCount
)。 - 引用计数修改:调用
retain
/release
时,运行时通过对象的isa
指针找到对应的SideTable
,加锁后修改refcnts
中的计数值。 - 弱引用注册:当对象被
__weak
指针指向时,运行时将其弱引用指针注册到SideTable
的weak_table
中。 - 对象释放:当引用计数减至 0 时,运行时触发
dealloc
,遍历weak_table
将所有弱引用置为nil
,然后释放SideTable
关联的资源(如refcnts
和weak_table
内存)。
3️⃣SideTable 的典型应用场景
3.1 弱引用(__weak
)的实现
__weak
修饰的变量不会增加对象的引用计数,但需在对象释放时自动置空。Side Table 是其核心实现载体:
流程:
- 当对象被
__weak
变量引用时,系统会在 Side Table 中为该对象创建条目。 __weak
变量的值存储为 Side Table 条目的索引(而非直接存储对象地址)。- 对象释放时,通过 Side Table 找到所有指向它的
__weak
变量,并将其置空。
3.2 关联对象(Associated Objects)
通过 objc_setAssociatedObject
和 objc_getAssociatedObject
设置的关联对象,其数据实际存储在 Side Table 中:
示例:
// 为 NSObject 实例添加关联对象(键为 "com.example.name")
NSString *name = @"张三";
objc_setAssociatedObject(obj, (__bridge const void *)(@"com.example.name"), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 从 Side Table 中读取关联对象
NSString *storedName = objc_getAssociatedObject(obj, (__bridge const void *)(@"com.example.name"));
底层逻辑:
关联对象的键(如 @"com.example.name"
)和值会被存储在 Side Table 的 associated_objects
字段中,键通过哈希映射快速查找。
3.3 KVO(键值观察)的实现
KVO 的核心是 动态生成子类 并重写 setter
方法,但观察者列表(observers
)的存储依赖 Side Table:
流程:
- 当对对象属性添加 KVO 观察时,系统会生成一个该对象的子类(如
NSKVONotifying_Obj
)。 - 子类的
isa
指针指向 Side Table 中的观察者列表条目。 - 属性值变化时,通过 Side Table 找到所有观察者并触发通知。
3.4 引用计数的优化存储
ARC 下,对象的引用计数(retainCount
)不再直接存储在对象内存中,而是通过 Side Table 的 refcount
字段统一管理:
- 引用计数增加(
retain
)时,更新 Side Table 中的refcount
。 - 引用计数减少(
release
)时,检查refcount
是否为 0,若为 0 则触发对象释放。
4️⃣SideTable特点
- 内存布局:紧凑高效
SideTable 的条目(Entry)在内存中是 连续存储 的,通过哈希表快速查找。每个条目的大小根据存储内容动态调整(如仅存储弱引用时,条目较小;存储关联对象时,条目较大)。
- 线程安全:锁保护
Side Table 的读写需保证线程安全,苹果通过 自旋锁(Spin Lock) 或 信号量(Semaphore) 实现:修改 Side Table(如添加/删除条目)时加锁;读取 Side Table(如获取弱引用列表)时加锁或使用无锁读取(CAS 操作)。
- 版本演进:从
__objc_sideTable
到objc_sideTable
早期 iOS 版本(如 iOS 9 前)使用 __objc_sideTable
结构体,存储方式为数组;iOS 10 后优化为哈希表(objc_sideTable
),提升了查找效率。
总结
Side Table 是 iOS 内存管理的“元数据中心”,通过集中存储对象的扩展信息(引用计数、弱引用、关联对象等),解决了传统对象内存布局的局限性。它的存在让 OC 能够高效支持 ARC、弱引用、KVO 等高级特性,是苹果内存管理优化的关键技术之一。