SideTable

SideTable 是个结构体,翻译成中文是“边桌”,用途是放一些小东西,在SideTable 里面主要是存放ObjC对象的引用计数和弱引用关系。

slock 自旋锁,用于SideTable的加锁/解锁
refcnts 存储引用计数信息的哈希表,isa_t中引用计数extra_rc溢出时使用
weak_table 弱引用表,存储弱引用关系的哈希表

struct SideTable {
// 自旋锁
    spinlock_t slock;
// 引用计数
    RefcountMap refcnts;
// 弱引用表
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

SideTables内部结构如下图:


SideTables

ObjC中都是通过SideTables()[this]这样的方式获取到对象对应的SideTable

static objc::ExplicitInit> SideTablesMap;

static StripedMap& SideTables() {
    return SideTablesMap.get();
}

全局SideTablesMap中存储着所有SideTables
SideTables()函数返回值是StripedMap类型,我们来看下StripedMap的定义。可以看到StripedMap是个模版哈希表,用指针作为key,传入的模版类型T作为value的类型。StripedMap代表以对象的地址和SideTable建立映射关系的哈希表。
SideTables()中重载了运算符[],从array[indexForPointer(p)].value可以看出indexForPointer()即为StripedMap的哈希函数。StripedMap内部存储空间只有8个元素长度(array[StripeCount])。也就是说SideTables()中最多只能存储8个SideTable。
StripedMap很容易出现哈希冲突。当出现冲突时,会使得一个SideTable,被多个对象共用。但是一个对象只会存在于一个SideTable里。
哈希算法很简单,indexForPointer()内部对地址分别做右移4位和9位,然后两个值进行异或运算(位运算性能性能更高),最后最关键的一步%StripeCount取余,避免数组越界。
SideTables的析构函数看出,在程序运行过程中,苹果不希望SideTables被释放。这也可以理解,全局只有最多8个SideTable元素,不会浪费内存,也为了避免频繁重新创建的开销,的确没有释放的必要。

// StripedMap is a map of void* -> T, sized appropriately 
// for cache-friendly lock striping. 
// For example, this may be used as StripedMap
// or as StripedMap where SomeStruct stores a spin lock.
template
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast>(this)[p]; 
    }
//...
};

下面是SideTable 中,weak_table弱引用表的定义。weak_entries存储弱引用表中所有的弱引用和指向对象的关系,weak_entries中每个元素,存储一个对象与所有指向该对象的弱引用关系。
一个SideTable可能会对应多个对象,而weak_entries表示一个对象的弱引用关系,所以weak_table中可能有多个weak_entries元素。

/**
 * 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;
// 数组长度-1,值为2的幂-1
    uintptr_t mask;
// 最大发生哈希冲突次数
    uintptr_t max_hash_displacement;
};

objc4中提供以下几个函数来操作弱引用表。

// 在弱引用表中增加一组弱引用指针和对象的配对
/// Adds an (object, weak pointer) pair to the weak table.
id weak_register_no_lock(weak_table_t *weak_table, id referent, 
                         id *referrer, WeakRegisterDeallocatingOptions deallocatingOptions);

// 在弱引用表中移除一组弱引用指针和对象的配对
/// Removes an (object, weak pointer) pair from the weak table.
void weak_unregister_no_lock(weak_table_t *weak_table, id referent, id *referrer);

// 清空弱引用表,当对象销毁时调用
/// Called on object destruction. Sets all remaining weak pointers to nil.
void weak_clear_no_lock(weak_table_t *weak_table, id referent);

weak_entry_t的定义如下,其中referent代表指向的对象,DisguisedPtr是对指针的泛型封装。
referent后面是一个共用体,两个不同结构体共用相同的内存,referrers和inline_referrers使用相同的内存起始地址。

因为weak_entry_t内部变量指针深度较深,所以Apple使用了DisguisedPtr泛型封装后的指针来简化代码结构。

  1. DisguisedPtr referent; 被引用对象指针,DisguisedPtr 代表objc_object对象的指针。(objc_object结构体代表最基础对象,里面只包含一个isa,objc_object指针等同于id类型。)
  2. weak_referrer_t *referrers。weak_referrer_t是二级指针,代表弱引用指针的地址,referrers是三级指针,代表存储弱引用指针地址的哈希表地址。
  3. weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]。inline_referrers数组中每个元素存储一个weak指针的地址。
template 
class DisguisedPtr {
    uintptr_t value;
//...
}

typedef DisguisedPtr weak_referrer_t;

struct weak_entry_t {
    DisguisedPtr referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2; // hash数组中的元素个数
            uintptr_t        mask; // 数组长度-1,值为2的幂-1
            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];
        };
    };

    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;
        }
    }
};

从以下的描述中可以看出,out_of_line_ness与inline_referrers[1]中低两位占用相同的内存。对于一个指针对齐过的weak_referrer_t(DisguisedPtr),最低两位只能是0b00或者0b11。因此,如果值是0b10,代表超过存储空间,使用动态数组referrers来存储弱引用指针地址,否则,使用静态数组inline_referrers[WEAK_INLINE_COUNT]来存储弱引用指针地址。

或者换句话说,union内有两种结构体,前一种用动态数组存储,后一种使用静态数组存储。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用后一种静态数组。当超过WEAK_INLINE_COUNT时,会将静态数组中的元素转移到动态数组中,并之后都是用动态数组存储。
out_of_line()方法通过判断out_of_line_ness是否为0b10,得出存储采用的是哪种结构。inline_referrers静态数组中每个元素对应一个weak_referrer_t(DisguisedPtr),内部存储的是弱指针内存地址,而弱指针内存地址经过指针对齐,结尾两位不会为0b10。

/**
 * 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.
 */
// 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.

我们可以从weak_register_no_lock()的实现来窥探weak_entry_tweak_register_no_lock()函数的作用是向弱引用表中注册一个弱引用。
可以看到entry = weak_entry_for_referent(weak_table, referent))试图从弱引用表(weak_table_t)中找到referent(弱引用指向对象)的关联的weak_entry_t,如果找到,调用append_referrer()将referrer(弱引用指针)加入到weak_entry_t中。
如果未找到,则创建新的weak_entry_t(new_entry),然后weak_grow_maybe()判断weak_entry_t是否需要扩容,最后weak_entry_insert()在弱引用表中加入新的new_entry。

/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    if (referent->isTaggedPointerOrNil()) return referent_id;
// ...
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }
    return referent_id;
}

下面总结了weak_register_no_lock()内部几个函数的作用:

  • weak_entry_for_referent():在弱引用表中查找和对象相关的entry。
  • append_referrer():在entry中增加一个弱引用记录。
  • new_entry():创建一个与指定对象关联的entry。
  • weak_grow_maybe():对弱引用表进行空间检查,若需要扩容则扩容。
  • weak_entry_insert ():在弱引用表中插入新的entry。

weak_unregister_no_lock()的具体实现如下。
同样使用weak_entry_for_referent()查找对象相关的entry,如果找到,调用remove_referrer()从entry中移除它。然后判断entry是否为空,如果为空,说明该对象已经没有任何弱引用指向,在弱引用表weak_table中移除该对象的entry(weak_entry_remove())。

/** 
 * Unregister an already-registered weak reference.
 * This is used when referrer's storage is about to go away, but referent
 * isn't dead yet. (Otherwise, zeroing referrer later would be a
 * bad memory access.)
 * Does nothing if referent/referrer is not a currently active weak reference.
 * Does not zero referrer.
 * 
 * FIXME currently requires old referent value to be passed in (lame)
 * FIXME unregistration should be automatic if referrer is collected
 * 
 * @param weak_table The global weak table.
 * @param referent The object.
 * @param referrer The weak reference.
 */
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;

    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        remove_referrer(entry, referrer);
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }

        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }

    // Do not set *referrer = nil. objc_storeWeak() requires that the 
    // value not change.
}
  • remove_referrer() 从entry中移除弱引用指针地址。
  • weak_entry_remove()从弱引用表中移除entry。
    需要特别注意的是,以上两个函数,第一个在移除过程中不会进行缩容,第二个在移除过程中,可能会触发缩容。weak_compact_maybe()是对弱引用表进行缩容函数。当容量大于1024,且装载的元素只占不到1/16时触发缩容。
    苹果这样设计,其实很好理解。对于一个对象,被太多弱引用指向的场景一般不会出现,且随着对象被释放,entry中的动态数组也将释放。但对于SideTable,在程序运行中并不会被释放,其中的weak_table也不会释放。这将导致weak_table内的weak_entries,可能在临时创建大量对象被弱引用时,weak_entries的容量迅速膨胀,而且当这些临时对象被释放后,weak_entries仍然占着大量空间。因此,在weak_entries过大且大量空间处于闲置时,有必要对weak_entries进行缩容。

关于weak_clear_no_lock()函数的分析,在 https://www.jianshu.com/p/cb7d9a4b85ba

// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    // Shrink if larger than 1024 buckets and at most 1/16 full.
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) {
        weak_resize(weak_table, old_size / 8);
        // leaves new table no more than 1/2 full
    }
}

前文已经提到,entry可能会使用静态数组存储弱引用指针地址,也可能使用哈希动态数组存储。下面看下append_referrer()内部是如何实现的加入弱引用指针地址的?

/** 
 * Add the given referrer to set of weak pointers in this entry.
 * Does not perform duplicate checking (b/c weak pointers are never
 * added to a set twice). 
 *
 * @param entry The entry holding the set of weak pointers. 
 * @param new_referrer The new weak pointer to be added.
 */
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) {
        // Try to insert inline.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    ASSERT(entry->out_of_line());

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

append_referrer()伪代码:

  1. 判断entry是否启用动态数组。未启用则进入步骤2,使用静态数组。启用则进入步骤4。
  2. 从下标0开始遍历静态数组,直到找到没有值的位置,将弱指针地址存入,结束。若没找到进入步骤3。
  3. 静态数组没找到位置,说明其已经满了,需要启用动态数组,并将静态数组填充到动态数组中,然后初始化动态数组的哈希参数,并将out_of_line_ness设置为REFERRERS_OUT_OF_LINE,表示开启动态数组。
    注意:此时的动态数组空间和静态数组一样大,仍然放不下新加入的弱引用指针地址。
    weak_referrer_t *new_referrers = (weak_referrer_t *) calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
    开辟动态数组内存空间,大小和静态数组一样,为WEAK_INLINE_COUNT。
  4. 对动态数组进行扩容判断,num_refs存储着动态数组中元素的数量,当num_refs >= 动态数组空间的3/4时候,需要扩容,调用扩容并加入弱指针地址函数grow_refs_and_insert(),结束。
    可以看到,如果是从步骤3进入步骤4,则一定会触发grow_refs_and_insert()函数。
#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
    return grow_refs_and_insert(entry, new_referrer);
}
  1. 到达这一步,说明动态数组不需要扩容,直接填充数据到动态数组即可。动态数组存储采用哈希方式,key是对象指针,begin是哈希函数运算后的结果。
    从描述可以看到,w_hash_pointer()是个只用于弱引用指针的哈希函数,将弱引用指针地址传入,计算结果 & entry->mask(动态数组大小-1),得出最终的哈希值,这种 & 方式相比 前面%计算方式,性能更好,但必须保证 entry->mask的值后面全部为1(比如0b001111,而不能0b0011011),不然会导致空间浪费。要保证entry->mask后面全为1,则必须保证动态数组的容量(entry->mask+1)必须要为2的幂(1,2,4,8...)。可以看到动态数组初次创建WEAK_INLINE_COUNT == 4,为2的幂,下文将会看到动态数组扩容也将保持2的幂。
/** 
 * Unique hash function for weak object pointers only.
 * 
 * @param key The weak object pointer. 
 * 
 * @return Size unrestricted hash of pointer.
 */
static inline uintptr_t w_hash_pointer(objc_object **key) {
    return ptr_hash((uintptr_t)key);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);

w_hash_pointer()函数内部调用ptr_hash()函数,是个ObjC通用的指针哈希函数。可以看到计算结果类似是个32位的固定随机数。w_hash_pointer()的作用就是将原数据可能和数组下标位置之间的关系打乱,简单理解就是重新洗牌。

// Pointer hash function.
// This is not a terrific hash, but it is fast 
// and not outrageously flawed for our purposes.

// Based on principles from http://locklessinc.com/articles/fast_hash/
// and evaluation ideas from http://floodyberry.com/noncryptohashzoo/
static inline uint32_t ptr_hash(uint64_t key)
{
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
  1. 从begin下标处开始,尝试插入元素。若遇到哈希冲突,则使用开放寻址法中的线性探测,对下标进行跑圈。其中用hash_displacement记录哈希冲突次数。
    若最终都没找到(回到begin),调用bad_weak_table()报错。但不会出现该情况,因为前面已经进行了扩容判断。
  2. 若哈希冲突次数hash_displacement,大于entry中的记录的最大哈希冲突次数(entry->max_hash_displacement),则同步更新entry->max_hash_displacement。
  3. 将弱指针地址插入到动态数组找到的下标位置中,并将记录的元素数量entry->num_refs加1。
/** 
 * Grow the entry's hash table of referrers. Rehashes each
 * of the referrers.
 * 
 * @param entry Weak pointer hash set for a particular object.
 */
__attribute__((noinline, used))
static void grow_refs_and_insert(weak_entry_t *entry, 
                                 objc_object **new_referrer)
{
    ASSERT(entry->out_of_line());

    size_t old_size = TABLE_SIZE(entry);
    size_t new_size = old_size ? old_size * 2 : 8;

    size_t num_refs = entry->num_refs;
    weak_referrer_t *old_refs = entry->referrers;
    entry->mask = new_size - 1;
    
    entry->referrers = (weak_referrer_t *)
        calloc(TABLE_SIZE(entry), sizeof(weak_referrer_t));
    entry->num_refs = 0;
    entry->max_hash_displacement = 0;
    
    for (size_t i = 0; i < old_size && num_refs > 0; i++) {
        if (old_refs[i] != nil) {
            append_referrer(entry, old_refs[i]);
            num_refs--;
        }
    }
    // Insert
    append_referrer(entry, new_referrer);
    if (old_refs) free(old_refs);
}

grow_refs_and_insert()函数是对entry中的哈希数组进行扩容,并插入新的弱引用指针地址。
可以看到new_sizeold_size2倍,保持2的幂,并使用calloc重新开辟动态数组空间,更新mask为new_size-1。将元素数量num_refs和哈希冲突次数max_hash_displacement初始化为0,并对每个元素重新进行append_referrer()操作。最后就旧的动态数组进行销毁(free())。

weak_entry_for_referent()函数内部实现与append_referrer()类似,这里就不进行分析。
weak_grow_maybe()是对弱引用表进行扩容,grow_refs_and_insert()是对entry进行扩容,两者内部实现基本一样,这里也不进行分析。
weak_entry_insert ()是在弱引用表中增加entry,append_referrer()是在entry中增加弱引用指针地址记录,两者内部实现基本一样,这里也不进行分析。

RefcountMap refcnts

refcnts是存储引用计数的哈希表,它的定义如下。DisguisedPtr是对指针类型的封装,代表objc_object对象的指针。

// RefcountMap disguises its pointers because we 
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap,size_t,RefcountMapValuePurgeable> RefcountMap;

DenseMap的定义如下,可以看到DenseMap定义在llvm-DenseMap.h文件中,DenseMapllvm中定义的哈希表,使用开放寻址法中的二次探测来解决哈希冲突(偏移为12,22,32...)。
可以看到RefcountMap依次在模版中传入DisguisedPtr,size_t,RefcountMapValuePurgeable三个类型参数。

  • DisguisedPtr:hash key类型采用objc_object的地址
  • size_t:value类型采用size_t,value存放引用计数
  • RefcountMapValuePurgeable:配置项,当value(引用计数)为0时,会自动从hash表中清除这条记录。
    DenseMap<>有四个数据成员,BucketsNumEntriesNumTombstonesNumBuckets,分别用于表示散列桶的起始地址,元素个数,被删除的桶个数,桶的个数。其中BucketsNumEntriesNumBuckets类似weak_table_t中的weak_entriesnum_entriesmask+1
template ,
          typename KeyInfoT = DenseMapInfo,
          typename BucketT = detail::DenseMapPair>
class DenseMap : public DenseMapBase,
                                     KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
  friend class DenseMapBase;

  // Lift some types from the dependent base class into this class for
  // simplicity of referring to them.
  using BaseT = DenseMapBase;

  BucketT *Buckets;
  unsigned NumEntries;
  unsigned NumTombstones;
  unsigned NumBuckets;
...
}

 struct RefcountMapValuePurgeable {
    static inline bool isPurgeable(size_t x) {
        return x == 0;
    }
};

你可能感兴趣的:(SideTable)