iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理相关的知识,希望对你有一定的参考价值。
一、SideTables 和 weak_table 的关系
- 在 runtime 中,有四个数据结构非常重要,分别是 SideTables,SideTable,weak_table_t 和 weak_entry_t。它们和对象的引用计数以及 weak 引用相关。
- 在 runtime 内存空间中,SideTables 是一个 64 个元素长度 8 个元素长度的 hash 数组,里面存储了 SideTable。SideTables 的 hash 键值就是一个对象 obj 的 address。
- 因此,一个 obj 对应了一个 SideTable,但是一个 SideTable,会对应多个obj,这是因为 SideTable 的数量只有 64 个,所以会有很多 obj 共用同一个SideTable。
- 在一个 SideTable 中,又有两个成员,分别是:
RefcountMap refcnts; // 对象引用计数相关 map
weak_table_t weak_table; // 对象弱引用相关 table
- SideTable 的两个成员:refcents 是一个 hash map,其 key 是 obj 的地址,而 value,则是 obj 对象的引用计数。
- weak_table 则存储了弱引用 obj 的指针的地址,其本质是一个以 obj 地址为 key,弱引用 obj 的指针的地址作为 value 的 hash 表,hash 表的节点类型是 weak_entry_t。
- SideTables,SideTable,weak_table_t 和 weak_entry_t 的数据结构的关系如下图:
二、底层分析
① SideTables
- SideTables 可以理解为一个全局的 hash 数组,里面存储了 SideTable 类型的数据,其长度为 64。
- SideTabls 可以通过全局的静态函数获取:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
- 可以看到,SideTabls 实质类型为模板类型 StripedMap。
② StripedMap
- 继续来看 StripedMap 模板的定义:
// StripedMap<T> is a map of void* -> T, sized appropriately
// for cache-friendly lock striping.
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
enum { CacheLineSize = 64 };
#if TARGET_OS_EMBEDDED
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 }; // ios 设备的StripeCount = 64
#endif
struct PaddedT {
T value alignas(CacheLineSize); // T value 64字节对齐
};
PaddedT array[StripeCount]; // 所有PaddedT struct 类型数据被存储在array数组中。iOS 设备 StripeCount == 64
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越界
}
public:
// 取值方法 [p]
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
// Shortcuts for StripedMaps of locks.
void lockAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.lock();
}
}
void unlockAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.unlock();
}
}
void forceResetAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.forceReset();
}
}
void defineLockOrder() {
for (unsigned int i = 1; i < StripeCount; i++) {
lockdebug_lock_precedes_lock(&array[i-1].value, &array[i].value);
}
}
void precedeLock(const void *newlock) {
// assumes defineLockOrder is also called
lockdebug_lock_precedes_lock(&array[StripeCount-1].value, newlock);
}
void succeedLock(const void *oldlock) {
// assumes defineLockOrder is also called
lockdebug_lock_precedes_lock(oldlock, &array[0].value);
}
const void *getLock(int i) {
if (i < StripeCount) return &array[i].value;
else return nil;
}
};
- 通过开头的英文注释 StripedMap is a map of void* -> T, sized appropriately,
可以知道, 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越界
}
- 可以看到把地址指针右移 4 位异或地址指针右移 9 位,然后最后的值取余 StripeCount,来防止 index 越界。
- StripedMap 的所有 T 类型数据都被封装到 PaddedT 中,之所以再次封装到 PaddedT (有填充的 T)中,是为了字节对齐,估计是存取 hash 值时的效率考虑:
struct PaddedT {
T value alignas(CacheLineSize); // T value 64字节对齐
};
- 接下来,PaddedT 被放到数组 array 中:
PaddedT array[StripeCount]; // 所有PaddedT struct 类型数据被存储在array数组中。iOS 设备 StripeCount == 64
- 苹果为 array 数组写了一些公共的存取数据的方法,主要是调用 indexForPointer 方法,使得外部传入的对象地址指针直接 hash 到对应的 array 节点:
// 取值方法 [p]
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
- 由于 SideTabls 是一个全局的 hash 表,因此当然必须要带锁访问,StripedMap 提供了一些便捷的锁操作方法:
// Shortcuts for StripedMaps of locks.
void lockAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.lock();
}
}
void unlockAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.unlock();
}
}
void forceResetAll() {
for (unsigned int i = 0; i < StripeCount; i++) {
array[i].value.forceReset();
}
}
void defineLockOrder() {
for (unsigned int i = 1; i < StripeCount; i++) {
lockdebug_lock_precedes_lock(&array[i-1].value, &array[i].value);
}
}
void precedeLock(const void *newlock) {
// assumes defineLockOrder is also called
lockdebug_lock_precedes_lock(&array[StripeCount-1].value, newlock);
}
void succeedLock(const void *oldlock) {
// assumes defineLockOrder is also called
lockdebug_lock_precedes_lock(oldlock, &array[0].value);
}
const void *getLock(int i) {
if (i < StripeCount) return &array[i].value;
else return nil;
}
- 可以看到,所有的 StripedMap 锁操作,最终是调用的 array[i].value 的相关操作。因此,对于模板的抽象数据 T 类型,必须具备相关的 lock 操作接口。
- 因此,要用 StripedMap 作为模板 hash 表,对于 T 类型还是有所要求的。而在 SideTables 中,T 即为 SideTable 类型,稍后会看到 SideTable 是如何符合 StripedMap 的数据类型要求的。
③ SideTable
- SideTable 主要存放 OC 对象的引用计数和弱引用相关信息。定义如下:
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 的定义很清晰,有三个成员:
-
- spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable;
-
- RefcountMap refcnts :以 DisguisedPtr<objc_object> 为 key 的 hash 表,用来存储 OC 对象的引用计数(仅在未开启 isa 优化或在 isa 优化情况下 isa_t 的引用计数溢出时才会用到)。
- weak_table_t weak_table : 存储对象弱引用指针的 hash 表,是 OC weak 功能实现的核心数据结构。
- 除此之外,苹果为 SideTable 还写了构造和析构函数:
// 构造函数
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
// 析构函数(看看函数体,苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误)
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
- 通过析构函数可以知道,SideTable 是不能被析构的。
- 最后是一堆锁的操作,用于多线程访问 SideTable, 同时也符合上面提到的 StripedMap 中关于 value 的 lock 接口定义:
// 锁操作 符合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);
④ spinlock_t slock
- spinlock_t 的最终定义,实际上是一个 uint32_t 类型的非公平的自旋锁。所谓非公平,就是说获得锁的顺序和申请锁的顺序无关,也就是说,第一个申请锁的线程有可能会是最后一个获得到该锁,或者是刚获得锁的线程会再次立刻获得到该锁,造成饥饿等待。 同时在 OC 中,_os_unfair_lock_opaque 也记录了获取它的线程信息,只有获得该锁的线程才能够解锁。
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
- 关于自旋锁的实现,苹果并未公布,但是大体上应该是通过操作 _os_unfair_lock_opaque 这个 uint32_t 的值,当大于 0 时,锁可用,当等于或小于0时,需要锁等待。
⑤ RefcountMap refcnts
- RefcountMap refcnts 用来存储 OC 对象的引用计数,它实质上是一个以 objc_object 为 key 的 hash 表,其 vaule 就是 OC 对象的引用计数。同时,当 OC 对象的引用计数变为 0 时,会自动将相关的信息从 hash 表中剔除。
- RefcountMap 的定义如下:
// RefcountMap disguises its pointers because we
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
- 实质上是模板类型 objc::DenseMap,模板的三个类型参数 DisguisedPtr<objc_object>,size_t,true 分别表示 DenseMap 的 hash key 类型,value 类型,是否需要在 value==0 的时候自动释放掉响应的 hash 节点。
- 而 DenseMap 这个模板类型又继承与另一个 Base 模板类型 DenseMapBase:
template<typename KeyT, typename ValueT,
bool ZeroValuesArePurgeable = false,
typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
: public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>
⑥ weak_table_t weak_table
- 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_table_t 是一个典型的 hash 结构,其中 weak_entry_t *weak_entries 是一个动态数组,用来存储 weak_table_t 的数据元素 weak_entry_t,剩下的三个元素将会用于 hash 表的相关操作。
- weak_table 的 hash 定位操作如下所示:
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 begin = hash_pointer(referent) & weak_table->mask; // 这里通过 & weak_table->mask的位操作,来确保index不会越界
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries); // 触发bad weak table crash
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) { // 当hash冲突超过了可能的max hash 冲突时,说明元素没有在hash表中,返回nil
return nil;
}
}
return &weak_table->weak_entries[index];
}
- 定位操作比较清晰,首先通过 hash_pointer 来尝试确定 hash 的初始位置:
size_t begin = hash_pointer(referent) & weak_table->mask;
- 注意:这里做了& weak_table->mask 位操作来确保 index 不会越界,这同平时用到的取余%操作是一样的功能,只不过这里改用了位操作,提升了效率。
- 然后,就开始对比 hash 表中的数据是否与目标数据相等 while (weak_table->weak_entries[index].referent != referent),如果不相等,则 index +1, 直到 index == begin 或超过了可能的 hash 冲突最大值。
⑦ weak_entry_t
- weak_table_t 中存储的元素是 weak_entry_t 类型,每个 weak_entry_t 类型对应一个 OC 对象的弱引用信息。
- weak_entry_t 的结构和 weak_table_t 很像,同样也是一个 hash 表,其存储的元素是 weak_referrer_t,实质上是弱引用该对象的指针的指针,即 objc_object **new_referrer , 通过操作指针的指针,就可以使得 weak 引用的指针在对象析构后,指向 nil。
// The address of a __weak variable.
// These pointers are stored disguised so memory analysis tools
// don't see lots of interior pointers from the weak table into objects.
typedef DisguisedPtr<objc_object *> weak_referrer_t;
- weak_entry_t 的定义如下:
/**
* 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; // 被弱引用的对象
// 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
union {
struct {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent) // 构造方法,里面初始化了静态数组
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
- weak_entry_t 的结构分析如下:
-
- DisguisedPtr<objc_object> referent :弱引用对象指针摘要,其实可以理解为弱引用对象的指针,只不过这里使用了摘要的形式存储(所谓摘要,其实是把地址取负)。
-
- union :是一个联合,union 有两种形式:定长数组 weak_referrer_t inline_referrers[WEAK_INLINE_COUNT] 和动态数组 weak_referrer_t *referrers。这两个数组是用来存储弱引用该对象的指针的指针的,同样也使用了指针摘要的形式存储。当弱引用该对象的指针数目小于等于 WEAK_INLINE_COUNT 时,使用定长数组。当超过 WEAK_INLINE_COUNT 时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。
-
- bool out_of_line(): 该方法用来判断当前的 weak_entry_t 是使用的定长数组还是动态数组,当返回 true,此时使用的动态数组,当返回 false,使用静态数组。
weak_entry_t& operator=(const weak_entry_t& other) :赋值方法
weak_entry_t(objc_object *newReferent, objc_object **newReferrer) :构造方法。
- bool out_of_line(): 该方法用来判断当前的 weak_entry_t 是使用的定长数组还是动态数组,当返回 true,此时使用的动态数组,当返回 false,使用静态数组。
三、定长数组/动态数组
- weak_entry_t 会存储所有弱引用该对象的指针的指针,存储类型为 weak_referrer_t,其实就是弱引用指针的指针,但是是以指针摘要的形式存储的:
typedef DisguisedPtr<objc_object *> weak_referrer_t;
- weak_entry_t 会将 weak_referrer_t 存储到 hash 数组中,而这个 hash 数组会有两种形态:定长数组/动态数组:
union {
// 动态数组模式
struct {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
}iOS之深入解析内存管理retain与release的底层原理
iOS之深入解析内存管理Tagged Pointer的底层原理