【iOS】weak底层实现
前言
__weak是iOS中的弱引用修饰符,具有两大特性:
- 不增加引用计数:weak指针指向对象时,对象的retainCount不会加1,不持有对象的生命周期。
- 自动置空:当对象的引用计数变为0,执行dealloc释放后,所有指向该对象的weak指针会被Runtime自动置为nil,彻底杜绝野指针崩溃。
Q:为什么weak会自动置nil?
A:普通指针保存对象是存储对象的内存地址,而weak则是Runtime在SideTable中内部维护了一张weak引用表,记录了所有指向该对象的weak指针地址。当对象引用计算归零准备销毁时,Runtime通过遍历weak表找到所有指向对象的weak指针并置nil,从而避免悬空指针访问导致崩溃。
weak数据结构
SideTable
SideTable是Runtime管理对象附加信息的数据结构。运行时会根据对象的地址,在全局SideTables中找到对应的表。
Runtime不是全局共用一个表,而是维护一组SideTables分片表(真机8张,模拟器64张),多个对象通过地址哈希映射到不同的SideTable。这样可以减少锁竞争,提升并发性能。
objc
struct SideTable {
spinlock_t slock; // 自旋锁os_unfair_lock,保证线程安全
RefcountMap refcnts; // 引用计数表,以对象地址为key存储额外的引用信息
weak_table_t weak_table; // 弱引用表,用于管理__weak指针,保存多个weak_entry_t
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
// SideTable是运行时全局管理的数据结构,生命周期和进程基本一致,误删直接崩溃
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void reset() { slock.reset(); }
// 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);
};
SideTable是复用共享的,多个对象可共用一张SideTable。进入SideTable内部使用refcnts和weak_table区分不同对象。
- refcnts:额外引用计数表。key为对象地址,value是这个对象溢出到SideTable中的额外引用计数。普通OC对象的引用计数优先存储在自身isa的extra_rc中,太多存不下时,剩余部分存在SideTable的refcnts中。而weak对象不增加引用计数,因此也不会在refcnts存引用计数。
- weak_table:弱引用表,存储单元是结构体weak_entry_t。
而StripeMap就是Runtime管理SideTable的容器结构。Runtime不会为每一个OC对象单独创建一张SideTable,放在数组array\[\]里,所有OC对象通过哈希算法,分散映射到数组中某一张SideTable。

核心算法indexForPointer的实现逻辑:使用对象指针地址参与哈希运算得到数组下标,进而定位得到当前对象归属的SideTable。
Q:那么为什么不直接连续排布SideTable,而是用哈希分散?
A:核心目的为规避CPU伪共享。
CPU缓存以缓存行为最小单位加载数据,如果多个SideTable在内存上紧密排布,很大可能会被装入同一个缓存航。而伪共享就是指多个线程修改不同数据,但这些数据恰巧位于同一个CPU缓存行中,导致多线程并发操作不同SideTable只要其中一个被修改,整个缓存行实效,触发频繁缓存刷新,严重降低并发性能。
因此StripeMap的核心优化思路为通过哈希打散对象归属,尽可能让不同SideTable落在不同CPU缓存行。每个SideTable自带一把锁,进而让SideTable减少锁竞争,大幅提升多线程并发能力。
weak_table_t
具体看一下弱引用表的结构。
objc
struct weak_table_t {
weak_entry_t *weak_entries; // 哈希数组,存储对应的弱引用对象信息
size_t num_entries; // 数组元素个数
uintptr_t mask; // 掩码,等于哈希数组长度-1
uintptr_t max_hash_displacement; // 存储可能发生哈希冲突的最大次数,判断是否出现逻辑错误
};
weak_table_t是典型的哈希结构,其中weak_entries是一个动态数组,存储weak_entry_t类型的元素,即弱引用对象信息。
weak_entry_t
再具体看一下weak_entry_t的结构。
objc
struct weak_entry_t {
// 用真实地址异或一个随机值生成伪装地址,使用时再还原,防止被误判为强引用
DisguisedPtr<objc_object> referent; // 被弱引用的对象的伪装地址
// weak_entry_t有两种存储模式:存少量weak指针的inline数组和存大量weak指针的哈希表
union {
// out-of-line模式
struct {
weak_referrer_t *referrers; // 外部数组,保存一组weak变量地址
uintptr_t out_of_line_ness : 2; // 位域,标记当前是否切换到out-of-line模式
uintptr_t num_refs : PTR_MINUS_2; // 位域,表示该模式下保存了多少weak referrer
uintptr_t mask; // 用于哈希表寻址
uintptr_t max_hash_displacement; // 记录当前哈希表中最大偏移量,查找某个referrer时,探测距离超过max_hash_displacement就认为不存在
};
// line模式
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
// 少量weak指针存在inline_referrers数组里
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
// 判断采用哪种模式
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
// 重载赋值运算符,让一个weak_entry_t需要被复制或迁移时,可直接拷贝整个结构体
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;
}
}
};
这里我们知道weak_entry_t使用union优化weak引用存储,具体有两种存储对象指针地址的模式:
- 内联模式:弱引用数量少时直接使用内部固定数组inline_referrers,避免内存分配。
- 外联模式:当弱引用数量超过阈值时,将weak指针地址迁移到动态数组referrers中
因此,我们可以得出weak_table_t本质是一个哈希表,被弱引用对象的伪装地址作为key,weak_entry_t作为value。weak_entry_t内部使用两种结构存储所有__weak指针变量的地址。
weak底层原理
weak的底层是Runtime维护的全局分片哈希表体系,以对象地址为key,所有指向该对象的weak指针地址集合为value。
全局分片在于Runtime把全局weak哈希表拆成多个小哈希表,让不同线程访问不同分片,减少锁竞争,提高并发性能。
接下来我们具体看一下weak的底层实现。
weak赋值流程
objc
__weak NSObject *weakObj = obj;
编译器不会直接做weakObj = obj赋值,普通赋值会产生强引用,它会转换成objc_initWeak(&weakObj, obj);。
objc
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
这里的本质是给__weak指针做初始化绑定,将弱引用添加到Runtime的weak表中。即初始化一个__weak变量,让它开始弱引用某个对象,然后把弱引用关系登记到Runtime的weak表中。
其中两个参数:
- location:表示变量本身的地址,即保存weak指针的内存地址。
- newObj:表示weak变量要指向的新对象。
storeWeak
然而真正的弱引用表操作全部委托给storeWeak。storeWeak是Runtime内部统一处理weak引用存储的函数,处理weak的写入和修改操作,同步弱引用关系。
objc
static id
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.
//storeWeak可能出现并发问题,如果发现并发冲突,重新执行
retry:
// 读取旧对象,找到旧对象对应SideTable
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 获取新对象SideTable
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 这里需要排序且加两个锁,同时涉及一个对象的删除和另一个对象的添加,且需要地址小的先锁,因此排序,防止死锁。
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 检查oldObj是否在加锁期间被别的线程修改
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();
// 检查类是否已经执行完initialize初始化
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
// 解锁避免死锁,防止出现线程A weak操作等待类初始化,线程B类初始化里面创建weak,等待weak锁造成死锁
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.
// 移除旧weak
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
// 注册新weak
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;
}
总结一下主要步骤:
storeWeak内部实际维护了三个参数haveOld、haveNew、crashIfDeallocating(监测目标对象是否处于正在释放状态。true直接触发崩溃,禁止给正在销毁的对象新增弱引用;false也拒绝注册,但不崩溃,返回nil)和两个哈希表oldTable、newTable。
当weak指针改变指向时,会先调用weak_unregister_no_lock解除旧引用关系,再通过weak_register_no_lock将新weak指针地址添加到弱引用表中,建立新关系,并通过setWeaklyReferenced_nolock修改weak新引用对象的bit标志位,标记对象存在weak引用。
weak_unregister_no_lock
该函数的主要作用在于移除对象和指针地址之间的映射关系。
objc
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
// 将参数转换为具体的对象指针和弱引用指针
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
// 在弱引用表中查找该对象对应的weak_entry_t
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 从weak_entry_t中移除一个weak变量地址referrer
remove_referrer(entry, referrer);
// 检查两种模式下的weak_entry_t是否为空
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
// 如果w该对象已经没有任何weak引用,就从weak_table_t中移除对应的weak_entry_t
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// 没有设置*referrer = nil,因为objc_storeWeak()要求在接触注册期间,弱引用变量的值不能变
// Do not set *referrer = nil. objc_storeWeak() requires that the
// value not change.
}
这里注意对比一下weak_unregister_no_lock和weak_clear_no_lock:
- weak_unregister_no_lock:weak变量要消失,如局部变量要被释放重新赋值。且删除后不置nil。
- weak_clear_no_lock:对象要被销毁时。且删除后所有weak置nil。
weak_register_no_lock
该函数的主要作用在于将一个新的__weak指针变量的地址登记到某个对象的弱引用表中。当对象销毁时,Runtime可以将这些weak变量自动置为nil。
主要步骤:
- 注册前检查对象是否正在dealloc销毁状态。如果对象已经进入释放流程,则直接拒绝建立,返回nil或报错。
- 检查是否存在weak_entry_t。如果已经存在,则调用append_referrer将新指针地址加入对应的referrers中;反之先创建新的weak_entry_t,再插入weak_table_t。
objc
id
// referent_id:被弱引用的对象
// referrer_id:weak变量本身的地址
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
// 如果referent为nil或者小对象类型,不做操作,直接返回
if (_objc_isTaggedPointerOrNil(referent)) return referent_id;
// ensure that the referenced object is viable
// 检查对象是否在释放
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
// 判断有没有自定义的引用计数方法,没有就走Runtime默认方式
if (!referent->ISA()->hasCustomRR()) {
// 如果返回true,说明已经处于正在释放状态,无法建立引用
deallocating = referent->rootIsDeallocating();
}
else {
// Use lookUpImpOrForward so we can avoid the assert in
// class_getInstanceMethod, since we intentionally make this
// callout with the lock held.
// 如果当前类自定义了引用计数方法,Runtime就会查找该对象是否实现allowsWeakReference方法
auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
referent->getIsa());
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
// 调用allowsWeakReference方法判断能否weak
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}
if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
}
// now remember it and where it is being stored
// 正常登记weak引用
weak_entry_t *entry;
// 检查是否有referent相关表
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 有就新增元素
append_referrer(entry, referrer);
}
// 没有就先建表
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
其中主要看一下以下几个函数实现:
- weak_entry_for_referent
该函数的作用在于根据对象referent在全局弱引用表weak_table_t中找到对应weak_entry_t。
objc
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
ASSERT(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
// 根据对象地址计算在哈希表里的起始下标,通过mask位操作保证index不会越界
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0; // 计算从原始哈希位置向后探测的步数
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
// 如果绕回起点,说明哈希表被异常填满,或者弱引用表结构被破坏
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
// max_hash_displacement记录所有entry发生过的最大偏移量,超过还没找到就返回nil
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
- append_referrer
该函数作用在于将一个新weak指针地址new_referrer添加到某个对象对应的weak_entry_t中。少量时直接存在inline数组里,数量多了就转成out_of_line哈希表,并通过线性探测解决哈希冲突。
objc
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
// 如果是inline模式,就放到定长数组中
if (! entry->out_of_line()) {
// Try to insert inline.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// 如果遍历未找到位置,说明inline存不下,转换为out_of_line模式
// Couldn't insert inline. Allocate out of line.
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it. // 这里构造的new_referrers不是合法哈希表,但后面的grow_refs_and_insert会重新扩容和哈希
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
// 将原来定长数组元素拷贝过来
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}
ASSERT(entry->out_of_line());
// 判断哈希表是否需要扩容。哈希表使用开放寻址和线性探测,如果表太满,会增加哈希冲突,查找插入效率降低
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
// 重新扩容,内部创建合法哈希表
return grow_refs_and_insert(entry, new_referrer);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
// 绕回起点说明哈希表结构被破坏
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
weak_referrer_t &ref = entry->referrers[index]; // 获取目标位置的引用
ref = new_referrer; // 赋值
entry->num_refs++; // 当前weak_entry_t中保存的weak引用数量同步增加
}
weak读取流程
objc
NSLog(@"%@", weakObj);
当我们读取一个weak变量时,编译器会调用objc_loadWeak()。
objc
id
objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}
核心逻辑为:
- 根据weak变量的地址在对应的weak_entry_t中找到被弱引用的对象referent,临时retain一次这个对象。防止当前线程读取的时候被其他线程释放导致返回一个已经释放的野指针。
- 再将对象加入到autoreleasepool中,保证前面临时增加的引用会延迟释放,即在自动释放池生命周期内保持有效。
- 最后返回对象。
weak释放流程
当对象的引用计数变为0时,底层会调用_objc_rootDealloc释放对象。
objc
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
rootDealloc
objc
inline void
objc_object::rootDealloc()
{
// 判断对象是不是Tagged Pointer,是则直接返回
if (isTaggedPointer()) return; // fixme necessary?
// We only get here through a msgSend, so the class is realized.
#if !ISA_HAS_INLINE_RC
_object_dispose_nonnull_realized((id)this);
#else
// 如果对象满足以下条件,直接快速释放:
if (fastpath(isa().nonpointer && // 1. 采用了优化isa计数方式
!isa().weakly_referenced && // 2. 对象没有被weak引用过
!isa().has_assoc && // 3. 没有关联对象
#if ISA_HAS_CXX_DTOR_BIT
// 4. 判读对象有没有C++成员析构函数
!isa().has_cxx_dtor &&
#else
!isa().getClass(false)->hasCxxDtor() &&
#endif
!isa().has_sidetable_rc)) // 5. 引用计数没有溢出到SideTable
{
assert(!sidetable_present());
free(this);
}
// 否则执行完整销毁流程
else {
_object_dispose_nonnull_realized((id)this);
}
#endif // ISA_HAS_INLINE_RC
}
weak_clear_no_lock
逐步调用objc_destructInstance_nonnull_realized、clearDeallocating、clearDeallocating_slow、weak_clear_no_lock方法。
该函数的主要作用在于当一个对象即将销毁时,Runtime调用该函数把所有指向这个对象的weak指针自动设置为nil,并删除这个对象对应的weak引用记录。
objc
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers; // 保存找到的weak指针的地址数组
size_t count; // 表示接下来要遍历的位置数
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
// 取出一个weak变量地址
objc_object **referrer = referrers[i];
if (referrer) {
// 如果weak变量仍指向当前对象,就置nil
if (*referrer == referent) {
*referrer = nil;
}
// 如果weak指针还在原对象的weak_referrent_t数组中,但是指向的不是当前对象就报错
else if (*referrer) {
REPORT_WEAK_ERROR("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak().",
referrer, (void*)*referrer, (void*)referent);
}
}
}
// 所有关系都断开后,移除weak_entry_t
weak_entry_remove(weak_table, entry);
}
总结
整个 weak 的底层可以归纳为四个核心函数:
- storeWeak():weak赋值总入口,负责整个流程控制。
- weak_register_no_lock():注册对象与weak指针地址的映射。
- weak_unregister_no_lock():解除旧的映射关系。
- weak_clear_no_lock():对象释放时遍历所有weak指针并置为nil。