weak 的用处
用一句话可归纳为:弱引用,在对象释放后置为 nil,避免错误的内存访问。用更通俗的话来表述是:weak 可以在不增加对象的引用计数的同时,又使得指针的访问是安全的。
SideTables, SideTable, weak_table
关系:
为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。
一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。
一个SideTable中,又有两个成员,分别是:
RefcountMap refcnts; // 对象引用计数相关 map
weak_table_t weak_table; // 对象弱引用相关 table
其中,refcents是一个hash map,其key是obj的地址,而value,则是obj对象的引用计数。
而weak_table则存储了弱引用obj的指针的地址,其本质是一个以obj地址为key,弱引用obj的指针的地址作为value的hash表。hash表的节点类型是weak_entry_t。
1.SideTables
先来说一下最外层的SideTables。SideTables可以理解为一个全局的hash数组,里面存储了SideTable类型的数据,其长度为64。
SideTables可以通过全局的静态函数获取:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
可以看到,SideTables 实质类型为模板类型StripedMap 。 StripedMap 是一个以void *为hash key, T为vaule的hash 表。hash定位的算法如下:
static unsigned int indexForPointer(const void *p) { // 该方法以void *作为key 来获取void *对应在StripedMap 中的位置
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // % StripeCount 防止index越界
}
2. SideTable
SideTable定义如下:
struct SideTable {
spinlock_t slock; // 自旋锁,防止多线程访问冲突
RefcountMap refcnts; // 对象引用计数map
weak_table_t weak_table; // 对象弱引用map
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
// 锁操作 符合StripedMap对T的定义
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
苹果为SideTable还写了构造和析构函数:
// 构造函数
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
//析构函数(看看函数体,苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误)
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
通过析构函数可以知道,SideTable是不能被析构的。
2.1 spinlock_t slock 自旋锁
它的作用是在操作引用计数的时候对SideTable加锁,避免数据错误。
自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。
2.2 RefcountMap refcnts
RefcountMap refcnts 用来存储OC对象的引用计数。它实质上是一个以objc_object为key的hash表,其vaule就是OC对象的引用计数。同时,当OC对象的引用计数变为0时,会自动将相关的信息从hash表中剔除。
2.3 weak_table_t weak_table
用来存储OC对象弱引用的相关信息。我们知道,SideTables一共只有64个节点,而在我们的APP中,一般都会不只有64个对象,因此,多个对象一定会重用同一个SideTable节点,也就是说,一个weak_table会存储多个对象的弱引用信息。因此在一个SideTable中,又会通过weak_table作为hash表再次分散存储每一个对象的弱引用信息。
weak_table_t定义如下:
/**
* 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; // hash数组,用来存储弱引用对象的相关信息weak_entry_t
size_t num_entries; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。
weak 实现原理
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
weak 的实现原理可以概括一下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。