前言
在objective-c中,weak几乎无处不在。尤其是定义ivar时,经常要用到这个关键字。用weak修饰的变量,在对象释放之后,对象会自动置为nil。它是怎么做到的呢?在研究它之前,有几个词有必要先了解一下。
关键词
SideTable
SideTable是一个结构体,主要有三个成员。它的作用就是用来管理对象的weak表,引用计数在此先不表。
struct SideTable {
// 自旋锁
spinlock_t slock;
// 引用计数的hash表
RefcountMap refcnts;
// 存储对象弱引用指针的hash表
weak_table_t weak_table;
...
}
其中weak表(weak_table)是实现weak功能的核心数据结构。其实也可以理解SideTable是对weak_table的一个封装,方便更好的使用和访问weak表。
weak表
weak表是一个hash表,存储了弱引用对象以及相关的所有弱引用的信息。key为对象的地址,value为指向该对象的weak指针地址数组。
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
抽象一点的话可以像下面这么理解:
f(对象的地址) = 指向该对象的weak指针地址数组
weak_entry_t
weak_entries也是一个hash结构体,它存储的是指向绑定的弱引用对象的所有weak指针的地址。
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
* is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
...
};
weak的实现原理
- ①初始化时,runtime调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
- ②objc_initWeak函数会调用storeWeak函数。更新指针指向,创建弱引用表。
- ③如果weak指针之前指向了一个弱引用对象,则调用weak_unregister_no_lock函数接触该对象和weak指针的绑定。
- ④如果weak指针需要指向一个新的弱引用对象,则调用weak_register_no_lock函数把weak指针地址添加到以该对象的地址为key的弱引用表里,从而实现绑定。
释放
- ①对象释放时调用release函数。
- ②然后调_objc_rootRelease函数。
- ③接着调rootRelease函数。
- ④引用计数为0时,执行dealloc函数。
- ⑤然后调_objc_rootDealloc函数。
- ⑥接着调rootDealloc函数。
- ⑦接着调object_dispose函数。
- ⑧接着调用objc_destructInstance函数。
- ⑨接着就是一个比较重要的函数clearDeallocating。
- ⑩接着调用clearDeallocating_slow函数。
clearDeallocating中有两个分支,现实判断对象是否采用了优化isa引用计数,如果没有的话则需要清理对象存储在SideTable中的引用计数数据。如果对象采用了优化isa引用计数,则判断是都有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow方法。
- ⑪最后调用关键的weak_clear_no_lock函数。从表中通过释放对象的地址拿到存放weak指针地址的弱引用表,然后将所有weak指针地址指向的值赋为nil,并从表中删掉该条记录。
总结
weak功能的实现主要就是通过runtime维护一张hash表,在表里存储对象的地址和指向它的所有weak指针的地址。当存储对象被释放时,通过weak_clear_no_lock函数遍历存储weak指针地址的数组,把weak指针置空,从而避免野指针的问题。(因为对象一般分配在堆上的某块内存,如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉;而基础数据类型一般分配在栈上,栈的内存会由系统自己自动处理,不会造成野指针)