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<objc_object> 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
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);
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
。