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) :构造方法。

三、定长数组/动态数组

  • 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之深入解析内存管理MRC与ARC机制

iOS之深入解析内存管理Tagged Pointer的底层原理

iOS之深入解析内存管理NSTimer的强引用问题

iOS之深入解析内存管理的引用计数retainCount的底层原理

iOS之深入解析Memory内存