由 布多(budo) 发布于 2023-10-21 • 最后更新于 2025-02-26
前言 weak 指针是 iOS 开发中一个非常基础的概念,在开发过程中我们经常使用它,它到底是怎么实现的?这篇文章将从 Runtime 源码入手,为你介绍 weak 指针的实现原理;让你知其然,更知其所以然。
weak 指针之编译期实现 当我们初始化一个 weak 指针时: __weak typeof(obj) weakObj = obj;
,编译器其实会把它们转换成类似这样的代码:objc_initWeak((void *)&weakObj, obj);
从上图的断点中我们也可以发现,weak 指针调用了 objc_initWeak
函数来完成初始化。在 Runtime 源码中我们可以找到 objc_initWeak
相关的实现细节。
本文使用的 Runtime 版本是 objc4-906 ,为了方便阅读,我对代码样式和排版略作了修改以及删减了一些不影响主逻辑的冗余代码。
我在 这里 维护了一个可以直接运行调试的 Runtime 项目,方便大家直接调试源码。
weak 指针之运行时实现 与 weak 指针初始化相关的函数有以下 4 个:
id objc_initWeak (id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak <DontHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, (objc_object*)newObj); } id objc_initWeakOrNil (id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak <DontHaveOld, DoHaveNew, DontCrashIfDeallocating>(location, (objc_object*)newObj); } id objc_storeWeak (id *location, id newObj) { return storeWeak <DoHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, (objc_object *)newObj); } id objc_initWeakOrNil (id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak <DontHaveOld, DoHaveNew, DontCrashIfDeallocating>(location, (objc_object*)newObj); }
关于函数 objc_initWeak 和 objc_storeWeak 的区别:
从以上 4 个初始化函数不难发现,它们最终都调用了同一个函数 storeWeak
,区别就是传递给函数的模板参数略有不同。
weak 指针的初始化细节:storeWeak storeWeak
函数的相关代码整理后如下所示:
enum HaveOld { DontHaveOld = false , DoHaveOld = true };enum HaveNew { DontHaveNew = false , DoHaveNew = true };enum CrashIfDeallocating { DontCrashIfDeallocating = false , DoCrashIfDeallocating = true }; template <HaveOld haveOld, HaveNew haveNew, enum CrashIfDeallocating crashIfDeallocating>static id storeWeak (id *location, objc_object *newObj) { Class previouslyInitializedClass = nil; id oldObj; SideTable *oldTable; SideTable *newTable; retry: if (haveOld) { oldObj = *location; oldTable = &SideTables ()[oldObj]; } else { oldTable = nil; } if (haveNew) { newTable = &SideTables ()[newObj]; } else { newTable = nil; } if (haveOld && *location != oldObj) { goto retry; } if (haveNew && newObj) { Class cls = newObj->getIsa (); if (cls != previouslyInitializedClass && !cls->isInitialized ()) { class_initialize (cls, newObj); previouslyInitializedClass = cls; goto retry; } } if (haveOld) { weak_unregister_no_lock (&oldTable->weak_table, oldObj, location); } if (haveNew) { newObj = weak_register_no_lock (&newTable->weak_table, (id)newObj, location, crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating); if (!_objc_isTaggedPointerOrNil(newObj)) { newObj->setWeaklyReferenced_nolock (); } *location = (id)newObj; } callSetWeaklyReferenced ((id)newObj); return (id)newObj; }
这个函数就是 weak 指针初始化的最终函数。从函数中不难发现,它主要就干了 2 件事:
调用 weak_unregister_no_lock
:将 weak 指针与当前对象解除弱引用关联。
调用 weak_register_no_lock
:将 weak 指针与新对象建立弱引用关联。
weak 指针解除关联的细节:weak_unregister_no_lock 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 ; if ((entry = weak_entry_for_referent (weak_table, referent))) { remove_referrer (entry, referrer); bool empty = true ; if (entry->out_of_line () && entry->num_refs != 0 ) { empty = false ; } else { for (size_t i = 0 ; i < 4 ; i++) { if (entry->inline_referrers[i]) { empty = false ; break ; } } } if (empty) { weak_entry_remove (weak_table, entry); } } } static weak_entry_t *weak_entry_for_referent (weak_table_t *weak_table, objc_object *referent) { weak_entry_t *weak_entries = weak_table->weak_entries; if (!weak_entries) return nil; 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++; if (hash_displacement > weak_table->max_hash_displacement) { return nil; } } return &weak_table->weak_entries[index]; } static void remove_referrer (weak_entry_t *entry, objc_object **old_referrer) { if (!entry->out_of_line ()) { for (size_t i = 0 ; i < 4 ; i++) { if (entry->inline_referrers[i] == old_referrer) { entry->inline_referrers[i] = nil; return ; } } return ; } size_t begin = w_hash_pointer (old_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0 ; while (entry->referrers[index] != old_referrer) { index = (index+1 ) & entry->mask; if (index == begin) bad_weak_table (entry); hash_displacement++; if (hash_displacement > entry->max_hash_displacement) { return ; } } entry->referrers[index] = nil; entry->num_refs--; } static void weak_entry_remove (weak_table_t *weak_table, weak_entry_t *entry) { if (entry->out_of_line ()) free (entry->referrers); memset (entry, 0 , sizeof (*entry)); weak_table->num_entries--; weak_compact_maybe (weak_table); }
代码稍微有点多,但整体的逻辑比较清晰。weak_unregister_no_lock
函数中主要做了 3 件事:
调用 weak_entry_for_referent
从弱引用表中获取指定的那张表数据。
调用 remove_referrer
从表中移除弱指针(即 referrer)。
检查表是否为空,是的话就调用 weak_entry_remove
从 weak_table 中移除这张表。
移除弱引用关联,本质上就是从 weak_table 中找到对象对应的弱引用数组,然后从数组中找到需要移除的 weak 指针并将其置空。weak_table 其实就是一个哈希表,关于哈希表的实现原理请自行了解。
weak 指针建立关联的细节:weak_register_no_lock id 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; if (deallocatingOptions == ReturnNilIfDeallocating || deallocatingOptions == CrashIfDeallocating) { bool deallocating; if (!referent->ISA ()->hasCustomRR ()) { deallocating = referent->rootIsDeallocating (); } else { auto allowsWeakReference = (BOOL (*)(objc_object *, SEL)) lookUpImpOrForwardTryCache ((id)referent, @selector (allowsWeakReference), referent->getIsa ()); if ((IMP)allowsWeakReference == _objc_msgForward) { return nil; } 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; } } } weak_entry_t *entry; 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); } return referent_id; } static void append_referrer (weak_entry_t *entry, objc_object **new_referrer) { if (!entry->out_of_line ()) { for (size_t i = 0 ; i < 4 ; i++) { if (entry->inline_referrers[i] == nil) { entry->inline_referrers[i] = new_referrer; return ; } } weak_referrer_t *new_referrers = (weak_referrer_t *) calloc (4 , sizeof (weak_referrer_t )); for (size_t i = 0 ; i < 4 ; i++) { new_referrers[i] = entry->inline_referrers[i]; } entry->referrers = new_referrers; entry->num_refs = 4 ; entry->out_of_line_ness = 2 ; entry->mask = 4 - 1 ; entry->max_hash_displacement = 0 ; } if (entry->num_refs >= (entry->mask + 1 ) * 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_for_referent
获取对应的表。然后在通过 weak 指针进行一系列哈希运算,从而拿到要添加/要删除的数组索引,最终对数组元素进行添加/删除操作。
截止到这里,关于 weak 指针的整个初始化过程已经全部讲完了。如果你还想了解更多细节的话可以继续阅读后面的内容。
深入剖析 SideTable 类型 SideTable 是用来存储弱引用数据和引用计数的一个数据结构,由于这篇文章只涉及弱引用相关问题,所以在后面的源码中我会特意去掉与文章不相关的内容,如果你想了解全部细节的话请阅读源码。
struct SideTable { weak_table_t weak_table; }; struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; uintptr_t mask; uintptr_t max_hash_displacement; }; struct weak_entry_t { id referent; union { struct { id *referrers; uintptr_t out_of_line_ness : 2 ; uintptr_t num_refs : 62 ; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { id inline_referrers[4 ]; }; }; };
你可以把 SideTable 理解成一个获取弱引用表的入口,weak_table 类似一个大哈希表,这个哈希表的 key 是 objc 对象,value 是对应的弱引用数组。关于 weak 指针的操作,例如解除关联和建立关联,其实就是从 weak_table 中获取其对应的弱引用数组,然后从这个数组中移除或添加对应的弱指针地址。
weak 指针自动赋值 nil 的实现细节:weak_clear_no_lock 众所周知,weak 指针在对象被释放之后会自动指向 nil,那么它到底是如何实现的呢?
关于释放流程的函数调用顺序这里就不具体展开了,在 objc4-906 版本中,其函数调用顺序如下:dealloc => _objc_rootDealloc => rootDealloc => object_dispose => objc_destructInstance => clearDeallocating => clearDeallocating_slow => weak_clear_no_lock,我们重点看一下最后一个函数 weak_clear_no_lock
的实现细节。
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) { return ; } weak_referrer_t *referrers; size_t count; if (entry->out_of_line ()) { referrers = entry->referrers; count = (entry->mask ? entry->mask + 1 : 0 ); } else { referrers = entry->inline_referrers; count = 4 ; } for (size_t i = 0 ; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } 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_remove (weak_table, entry); }
从源码中不难发现,系统会在对象释放的时候,获取其对应的弱引用表,然后遍历这个表中的 weak 指针并将其赋值为 nil。
到此为止,关于 weak 指针的所有内容就讲完了。
总结 在 runtime 初始化的时候,会调用一个全局函数 side_tables_init
初始化一个全局数组,数组的元素是 SideTable 对象,可以通过全局函数 SideTables
拿到这个数组以及对象对应的 SideTable 对象。
SideTable 中有一个变量 weak_table,你可以将它理解成一个哈希表,哈希表的 key 是对象,value 是一个数组,数组中的元素就是指向这个 key 的 weak 指针地址。
weak 指针的初始化操作就是拿到这个对象对应的弱指针数组,然后往数组里面把 weak 指针的内存地址添加进去。
如果 weak 指针需要指向别的对象,需要拿到旧对象对应的弱指针数组并将数组中存放 weak 指针的那个位置置空,然后拿到新对象对应的弱指针数组并将 weak 指针添加进去。
如果对象释放了,就拿到这个对象对应的弱指针数组并挨个将里面的 weak 指针赋值为 nil。
为了加深自己的理解,我模仿系统的实现写了一个示例项目 WeakPointer ,我在这个项目里还给分类属性也支持了 weak 特性,感兴趣的同学可以参考一下。
关于 weak 指针的一些疑问与解答 为什么不能给 Category 添加 weak 属性? 我们一般是这样初始化一个 weak 指针:__weak id weakPtr = obj;
,从源码中我们知道编译器会把代码转换成这样:objc_storeWeak((void *)&weakPtr, obj);
。
从这里可以发现,要实现 weak 特性,你必须能拿到 obj 对象和 weak 指针的内存地址,而 Category 中的属性是依靠 runtime 中的 objc_setAssociatedObject
和 objc_getAssociatedObject
这 2 个函数来实现的,我们拿不到 weak 指针的内存地址,故而无法给 Category 的属性支持 weak 特性。
类的属性之所以支持 weak 特性,是因为编译器能拿到这个属性的成员变量的地址(即 weak 指针的内存地址)。
如果你一定要给 Category 添加 weak 属性的话,有以下 2 个思路(建议选择第 2 个):
参考我的这个项目 WeakPointer 模仿系统的实现手动维护一个弱引用表来支持 Category 的 weak 属性。
创建一个中间类,给中间类声明一个 weak 属性,Category 的属性强引用这个中间类,中间类的 weak 属性指向真正的对象。
示例代码:
@interface WeakTarget : NSObject @property (nonatomic , weak ) id weakObj;@end @interface Person (Category )@property (nonatomic , weak ) id weakObj;@end @implementation Person (Category )- (void )setWeakObj:(NSObject *)obj { WeakTarget *target = [[WeakTarget alloc] init]; target.weakObj = obj; objc_setAssociatedObject(self , @selector (weakObj), target, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id )weakObj { WeakTarget *target = objc_getAssociatedObject(self , @selector (weakObj)); return target.weakObj; } @end @implementation WeakTarget @end
为什么在 block 中不能使用 weak 指针访问其成员变量。 这是我在项目中实际遇到的一个问题,伪代码如下:
__weak typeof (self ) weakSelf = self ; dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ weakSelf->_propertyName; });
上面的代码无法通过编译,如果改成这样就没问题了:weakSelf.propertyName;
。
众所周知,在 objc 里访问属性最终还是会访问成员变量。那为什么访问属性就正常,访问成员变量就会报错呢?
之所以会这样,是因为现在的编译器比较智能,考虑的比较多。weakSelf 在运行时有可能为 nil 从而导致崩溃,编译器认为这样的代码不安全所以报错。但是使用 weakSelf 访问属性是安全的,因为访问属性实际上是调用了属性的 get/set 方法,在 objc 里对 nil 调用方法是不会导致异常。
上面的报错代码可以改成以下代码来解决编译报错:
dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ if (weakSelf) { __strong typeof (weakSelf) strongSelf = weakSelf; strongSelf->_propertyName; } });
为什么在对象没有弱引用时也会执行 weak_clear_no_lock 在研究 weak 指针自动赋值 nil 的过程中,我发现,对象只要曾经被 weak 指针指向过,在对象释放的时候即使没有指向它的 weak 指针,也会执行到 weak_clear_no_lock 函数。
在 storeWeak 函数中会调用这行代码设置对象被 weak 指针指向的标记:newObj->setWeaklyReferenced_nolock();
。
但是,当对象没有任何 weak 指针指向时,weak_unregister_no_lock 函数中并没有调用相关函数将标记设置为 false。
这会导致在对象释放的时候,即调用到 rootDealloc 函数时无法执行快速释放逻辑。
inline void objc_object::rootDealloc () { if (isTaggedPointer ()) return ; if (fastpath (isa ().nonpointer && !isa ().weakly_referenced && !isa ().has_assoc && !isa ().getClass (false )->hasCxxDtor () && !isa ().has_sidetable_rc)) { free (this ); } else { object_dispose ((id)this ); } }
我也不太清楚为什么这么做?如果你知道其中的具体细节的话,还请留言告知。