我们都知道,weak的主要作用是为了防止循环引用,而产生循环引用的根本原因则在于ARC下的引用计数错误问题,即两个对象或者多个对象相互持有,会造成超出作用域后引用计数不会减为0的现象。而weak和strong不同,它并不会增加对象的引用计数。
循环引用在ARC下,是不可避免的,于是weak也就应运而生了,与其说weak是弱引用,倒不如说weak是独立于引用计数之外的内存管理机制。
常见的weak使用场景一般有如下两个:
__weak wObj = obj;
@property(nonatomic,weak)id obj;
实际上这两个是一样的,写法不同而已,但是还是稍微有一点点区别,经过断点调试发现,__weak wObj = obj;
方法在运行时调用的是objc_initWeak
而@property(nonatomic,weak)id obj;
则调用的是objc_storeWeak
。
这两者的区别在于下一步调用的storeWeak
方法中old参数的不同,所以答案也显而易见,我们使用属性的时候,编译器会在类初始化的时候,完成属性里成员变量的声明,从而调用objc_initWeak
.
当我们使用weak的时候,实际上runtime会自动调用storeWeak
函数,查询源码我们会发现整个weak的实现历程。
大致的调用过程很简单,并没有什么出奇的地方,然而当我们下潜到具体的数据结构和函数实现的时候,才能感觉到weak设计的美妙之处。
全局散列表
调用完主函数 storeWeak
之后,第一个出现的函数是&SideTables()
这样一个很怪的C++函数:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
本函数的含义在于获取StripedMap<SideTable>
类型的全局静态变量。
StripedMap<SideTable>
为C++的模板类,类似于我们常用的泛型。
StripedMap类的实现非常有意思。
让我们回到主函数内,oldTable = &SideTables()[oldObj];
调用完&SideTables
之后,紧接着一个中括号是什么操作?C++里貌似只有数组才能用中括号,进入StripedMap类中看一下,发现如下操作:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
原来是运算符重载,PaddedT array[StripeCount];
内部实际上获取了array这个数组的成员。
至于数组类型,当然就是我们的SideTable
。
struct PaddedT {
T value alignas(CacheLineSize);
};
alignas作用:内存对齐。
获取下标的方式(散列函数的实现):中括号中的oldObj在这里是const void *p,可以看到这里的p是一个的指针。
oldObj是id类型,即objc_object *类型,是一个指向objc_object的指针.
利用p指针,入indexForPointer
的参:
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
StripeCount
为64 ,利用p的地址对64取余的意义在于既能防止数组越界,又能保证同一个地址获取的下标一致(确定性和散列碰撞)。
至此&SideTables()
函数的实现分析完毕,查找一个长度为64的全局的散列表,获取SideTable。
SideTable
主函数继续执行,执行到两个关键函数:
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,(id)newObj, location,CrashIfDeallocating);
主要用到的是SildeTable中的weak_table,而weak_table则又是一个hash表,里面存储的weak_entry_t真正存储了newObj实体DisguisedPtr<objc_object> referent;
这两个关键函数的意义就是将旧的obj从SildeTable中移除,再将新的obj加入SildeTable中。
而两个函数实现的关键点在于获取weak_table中的weak_entry_t实体,调用的同一个方法weak_entry_for_referent:
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;
// 散列函数
size_t index = hash_pointer(referent) & weak_table->mask;
size_t hash_displacement = 0;
// 查找
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
将查找到的weak_entry返回。
weak_entry_t
weak_entry_t的结构如下:
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
第一个成员DisguisedPtr<objc_object> referent;
就是我们主函数里id类型的对象(对象被保存在了这里)。
第二个成员是c++里的共用体,我们先看看它储存的是什么,在weak_register_no_lock函数中有对weak_entry_t的初始化代码:
weak_entry_t new_entry;
new_entry.referent = referent;
new_entry.out_of_line = 0;
new_entry.inline_referrers[0] = referrer;
for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) {
new_entry.inline_referrers[i] = nil;
}
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
共用体储存的是referrer,它就是主函数中的location,即我们用__weak修饰的对象指针,这里的作用是在对象释放的时候,也能查找到与之关联的weak修饰的对象指针,并对其进行置空操作,防止野指针。
所以我们用多个weak指向同一个对象的操作,是非常安全的。
weak释放
由于weak的内存管理游离于arc/mrc之外,故在arc生效并且没有错误操作的情况下,全局的散列表仍然持有对象的引用。
所以在每个对象调用dealloc方法的时候,会对当前对象所在的weak表进行clear操作,具体函数如下:
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;
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;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
这么长的一段代码,其实只有两个操作:
1.*referrer = nil;
将weak指针置空,防止野指针。
2.weak_entry_remove(weak_table, entry);
删除entry,并且对内存清空。
至此,对象和weak表的关系也就荡然无存了,对象完成了释放,weak表的中的weak引用通过对象的地址在两个散列表中查找到后,同样完成了释放。
总结
简单来说,arc下的引用操作往往伴随着引用计数的变化,而引用计数又绕不开循环引用这个诟病,所以weak其实就是一种不引起引用计数变化的"弱引用"机制。
但从weak设计的数据结构而言,可以分为最外层的全局StripedMap表,存储其中的SlideTable表,而SlideTable中,又有weak_referrer_t数组,来存储weak引用,多级结构设计,也让源码的阅读更有意思。