浅析weak指针的实现

浅析weak指针的实现

oc中weak指针主要用于打破循环或者防止循环引用的发生,应用场景还是很广泛的。那么被weak修饰的指针与被指向的对象在底层的运作机制究竟怎样的呢?为什么在对象释放销毁时weak指针能自动置为nil,从而避免了野指针的错误?

weak指针实现原理

当对象被一个weak指针引用时,底层的实现原理就是:不对被引用的对象进行retain,而是利用哈希表对weak指针与被指向的对象进行标记、关联。当对象销毁释放内存管时通过之前的标记对weak指针地址进行查找,最后把weak指针的指向置为nil。

weak指针实现源码分析

首先weak实现的函数调用顺序如图

完成weak指针标记需要处理的对象顺序如下图所示:

关于SideTable以及根据对象指针查找对应SideTable的分析在之前分析对象引用计数的文章中有相关的说明,这里就不再重复了。这里主要分析下处理weak_entry_t结构体的函数

id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating) { // now remember it and where it is being stored weak_entry_t *entry; if ((entry = weak_entry_for_referent(weak_table, referent))) { //if 判断 是否把weak指针指向的对象是否已经有对应的entry,有的话代表对象以前有被弱引用指向 append_referrer(entry, referrer); //把 referrer 存进 对应的 entry } else { //对象第一次被弱指针引用 weak_entry_t new_entry(referent, referrer); weak_grow_maybe(weak_table); //weak_table.weak_entries 扩容处理 weak_entry_insert(weak_table, &new_entry); //吧 weak_entry_t 放入 weak_table.weak_entries中数组,通过referent的哈希运算&mask得到索引 } // Do not set *referrer. objc_storeWeak() requires that the // value not change. return referent_id; } 复制代码

这里的逻辑主要是通过判断查找对象对应SideTable的weak_table属性中是否包含有对应的weak_entry_t结构体,查找的实现主要是通过把对象指针经过一定的哈希算法运算后与上一个特定的数值(该数值通常是要查找顺序表的最大索引)后得出的索引,再根据索引查找到特定的值与要查找的值对比得出结果。具体逻辑可以从源码中看weak_entry_for_referent这个函数的实现。

对象第一次被弱引用指向处理

上面的else语句就是处理对象第一次被弱引用指向的处理。通过生成一个weak_entry_t结构体后,把其插入到weak_table中,插入的逻辑与上面判断是否含有特定weak_entry_t的逻辑都是一样的,通过哈希算法获得所以,然后插入相应的索引位置,这里还有一个对weak_table进行扩容的处理weak_grow_maybe(weak_table);主要是判断weak_table的存放对象容量大于或等于总容量的3/4时对weak_table的进行两倍容量的扩容后,再把旧数据复制到扩容后的内存中。

对象之前已经被弱引用指向过

这种情况就相对复杂一点,首先我们看下最终存放weak指针地址的结构体定义

struct weak_entry_t { //被引用的对象指针(经过包装处理) DisguisedPtr referent; //联合体,用于存放weak指针地址 union { struct { weak_referrer_t *referrers; //8字节 当inline_referrers不够存放数据的时候,使用该指针在堆上开辟空间存放数据 uintptr_t out_of_line_ness : 2; uintptr_t num_refs : PTR_MINUS_2;//8字节 uintptr_t mask;//8字节 uintptr_t max_hash_displacement;//8字节 }; struct { // out_of_line_ness field is low bits of inline_referrers[1] weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // 32字节 }; }; } 复制代码

weak_entry_t结构体主要是通过上面的联合体(union)来存放对象的weak指针地址。代码中的联合体分为两部分,内存大小为64个字节,在添加weak指针的时候优先使用的是inline_referrers数组存放,如果数组的已经存满了数据(4个指针),就会用上面的结构体的数组指针来在堆上开辟空间来存放数据,这样就可以存放较多的weak指针地址。

weak指阵自动置nil原理

如果对象有被weak指针指向的话,在对象销毁释放内存执行-dealloc方法的时候,根据函数的调用顺序会执行到weak_clear_no_lock这个方法。我们来看下次方法是怎样使weakPointer = nil

参考文献:K码农-http://kmanong.top/kmn/qxw/form/home?top_cate=28void 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); weak_referrer_t *referrers; 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) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } } } } weak_entry_remove(weak_table, entry); } 复制代码

可以看出上面代码主要是获取到对应对象的weak_entry_t *entry,根据其内部结构体获取到存放的weak指针地址的个数,然后遍历存放的weak指针的内存,把weak指针置为nil。