sideTables

主要参考文章

iOS管理对象内存的数据结构以及操作算法–SideTables、RefcountMap、weak_table_t-一
Objective-C runtime机制(7)——SideTables, SideTable, weak_table, weak_entry_t
Runtime源码剖析—图解引用计数与weak
文中一些源码的完整版

一览图

在这里插入图片描述

在这里插入图片描述

使用背景

iOS的自动释放内存机制——自动引用计数。
互相强引用造成循环引用,无法合理释放内存,引入了弱引用(weak),弱引用只指向对象但不持有对象,所以弱引用指向的内存如果被释放就会造成野指针。
1、如何实现的引用计数管理,控制加一减一和释放?
2、如何维护weak指针防止野指针错误?
以上两个加上自旋锁就是sideTable结构体中存放的内容。
全局的sideTables,其中的每一个对象对应一个sideTable。

sideTables

sideTables

概念

sideTables全局的Hash表,用来装SideTable结构体。

sideTables:



static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
这里的方法名是sideTables,返回值是StripedMap<SideTable>&
//这里面其实StripedMap里一共就一个PaddedT类型的数组,大小为StripeCount【64】
//可以大致理解为,该方法会返回一个StripedMap类型的数据结构,里面存储的类型是SideTable

sideTablesMap:

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
//StripedMap<T> 是一个模板类,根据传递的实际参数决定其中 array 成员存储的元素类型。
// 能通过对象的地址,运算出 Hash 值,通过该 hash 值找到对应的 value 。

返回类型:StripedMap

template<typename T>
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<uintptr_t>(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<StripedMap<T>>(this)[p]; 
    }
    //省略其他方法
    

主要看这两句

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

    PaddedT array[StripeCount];

也就是说StripedMap这个类中维护了一个长度为StripeCount的结构体,结构体中只有一个成员,就是传进来的T类型,在这里是sideTable。

StripedMap 是一个以void *为hash key, T为vaule的hash 表。

什么是Hash表

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射。

Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。

如果两个散列值不相同(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(可能出现哈希碰撞
)。

为什么分为多个sideTable

为什么不是一个SideTable而是多个SideTable?或者(为什么不将所有的对象放到一个table里面,而是放到不同的side-table里面?)

  • 查找或者修改引用计数的时候是要加锁的,如果有多个对象同时查找引用计数:

  • 只有一张表的话,查询肯定是需要加锁,同步有先后顺序的;
    如果是有多张表,就可以异步进行查询,不同的表之间查询是没有影响的;–> 效率更高

sideTables到sideTable

StripedMap 是一个以void *为hash key, T为vaule的hash 表。
使用对象的内存地址当它的key。
SideTables[key]

sideTable

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //referanceCount:引用计数表
    weak_table_t weak_table;//弱引用表:存放的对象的弱引用指针
};

三个参数

参数一:自旋锁

用来标记只能有一个线程访问该对象,在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁(繁忙等待)。

苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁。

参数二:RefcountMap refcnts

对象具体的引用计数数量是记录在这里的。

// 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节点,这里是true。

而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> 
sideTable到RefcountMap

是一个一层结构,可以通过key直接找到对应的value。RefcountMap依旧是表,所以是able.refcnts.find(0x0000)

RefcountMap:四个部分(一层结构)

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable>
RefcountMap;

DenseMap又是一个模版类

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> {
  
  typedef DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable> BaseT;
  typedef typename BaseT::BucketT BucketT;
  friend class DenseMapBase<DenseMap, KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>;

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

列举几个比较重要的成员变量:

  1. ZeroValuesArePurgeable默认值是false,但 RefcountMap 指定其初始化为true. 这个成员标记是否可以使用值为 0 (引用计数为 1) 的桶. 因为空桶存的初始值就是 0, 所以值为 0 的桶和空桶没什么区别. 如果允许使用值为 0 的桶, 查找桶时如果没有找到对象对应的桶, 也没有找到墓碑桶, 就会优先使用值为 0 的桶.

  2. Buckets指针管理一段连续内存空间, 也就是数组, 数组成员是BucketT 类型的对象, 我们这里将 BucketT 对象称为桶(实际上这个数组才应该叫桶, 苹果把数组中的元素称为桶应该是为了形象一些, 而不是哈希桶中的桶的意思). 桶数组在申请空间后, 会进行初始化, 在所有位置上都放上空桶(桶的 key为 EmptyKey 时是空桶), 之后对引用计数的操作, 都要依赖于桶.
    引用计数结构bucketT实际上是std::pair,类似于isa,使用其中几个bit来保存引用计数,留出几个bit用来做其他标记位

    1. (1UL<<0) WEAKLY_REFERENCED
      表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

    2. (1UL<<1) DEALLOCATING
      表示对象是否正在被释放。1正在释放,0没有。

    3. REAL COUNT
      REAL COUNT的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四,因为真正的计数是从2^2位开始的。

    4. (1UL<<(WORD_BITS-1)) SIDE_TABLE_RC_PINNED
      其中WORD_BITS在32位和64位系统的时候分别等于32和64。随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。

  3. umEntries记录数组中已使用的非空的桶的个数.

  4. NumTombstones,Tombstone直译为墓碑, 当一个对象的引用计数为0, 要从桶中取出时, 其所处的位置会被标记为 Tombstone.NumTombstones就是数组中的墓碑的个数. 后面会介绍到墓碑的作用.

  5. NumBuckets桶的数量, 因为数组中始终都充满桶, 所以可以理解为数组大小

RefcountMap工作逻辑
  1. 过计算对象地址的哈希值, 来从SideTables中获取对应的SideTable. 哈希值重复的对象的引用计数存储在同一个 SideTable里.
  2. SideTable 使用find() 方法和重载 [] 运算符的方式, 通过对象地址来确定对象对应的桶. 最终执行到的查找算法是LookupBucketFor().
template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
  const BucketT *BucketsPtr = getBuckets();
  const unsigned NumBuckets = getNumBuckets();

  //如果桶的个数是0
  if (NumBuckets == 0) {
    FoundBucket = 0;
    return false;//返回false,回上层调用添加函数
  }

  // FoundTombstone - Keep track of whether we find a tombstone or zero value while probing.
  const BucketT *FoundTombstone = 0;
  const KeyT EmptyKey = getEmptyKey();
  const KeyT TombstoneKey = getTombstoneKey();
  assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
         !KeyInfoT::isEqual(Val, TombstoneKey) &&
         "Empty/Tombstone value shouldn't be inserted into map!");

  unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);//将哈希值与数组最大下标按位与
  unsigned ProbeAmt = 1;//哈希值重复的对象需要靠它来重新寻找位置
  
  while (1) {
    const BucketT *ThisBucket = BucketsPtr + BucketNo;//头指针+下标,类似于数组取值
    //找到桶中的key和对象地址相等,则是找到了
    if (KeyInfoT::isEqual(Val, ThisBucket->first)) {
      FoundBucket = ThisBucket;
      return true;
    }
    //找到的桶中的key是空桶占位符,则表示可插入
    if (KeyInfoT::isEqual(ThisBucket->first, EmptyKey)) {
      if (FoundTombstone) ThisBucket = FoundTombstone;//如果曾遇到墓碑,则使用墓碑的位置
      FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;//找到空占位符,则表明表中没有已经插入了该对象的桶
      return false;
    }
    //如果找到了墓碑
    if (KeyInfoT::isEqual(ThisBucket->first, TombstoneKey) && !FoundTombstone)
      FoundTombstone = ThisBucket;  //记录下墓碑
    //这里涉及到最初定义 typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap, 传入的第三个参数 true
      //这个参数代表是否可以清除 0 值, 也就是说这个参数为 true 并且没有墓碑的时候, 会记录下找到的 value 为 0 的桶
    if (ZeroValuesArePurgeable  && 
        ThisBucket->second == 0  &&  !FoundTombstone) 
      FoundTombstone = ThisBucket;

    //用于计数的ProbeAmt如果大于数组容量,就会抛出异常
    if (ProbeAmt > NumBuckets) {
      _objc_fatal("Hash table corrupted. This is probably a memory error "
                  "somewhere. (table at %p, buckets at %p (%zu bytes), "
                  "%u buckets, %u entries, %u tombstones, "
                  "data %p %p %p %p)", 
                  this, BucketsPtr, malloc_size(BucketsPtr), 
                  NumBuckets, getNumEntries(), getNumTombstones(), 
                  ((void**)BucketsPtr)[0], ((void**)BucketsPtr)[1], 
                  ((void**)BucketsPtr)[2], ((void**)BucketsPtr)[3]);
    }
    BucketNo += ProbeAmt++;//本次哈希计算得出的下标不符合,则利用ProbeAmt寻找下一个下标
    BucketNo&= (NumBuckets-1);//得到新的数字和数组下标按最大值按位与
  }
}



墓碑的作用

在哈希算法中,假设a和b对应的地址相等,比如都存放在1处,此时a已经存放在1处,b只能继续找,找到了3,存放在3处,此时a被释放,如果直接置空,当b的引用计数要改变时,它按照算法找到1处,此时1已经为空,所以会直接放到1处,会造成错误,所以墓碑的作用:

a被销毁后,在1处设立墓碑,当b通过哈希找到1处,发现是墓碑,就会继续找,找到3处,存放
且对于新的对象,发现此处是墓碑,继续往后找,发现没有存储过,就会放到墓碑处,所以也不影响新对象的存储。

参数三:weak_table_t weak_table

弱引用表
是一个两层结构。

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

weak_entries: hash数组,用来存储弱引用对象的相关信息weak_entry_t
num_entries: hash数组中的元素个数
mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)

weak_table_t的哈希操作

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

哈希数组
首先begin尝试确定hash的初始位置。注意,这里做了& weak_table->mask 位操作来确保index不会越界,这同我们平时用到的取余%操作是一样的功能。只不过这里改用了位操作,提升了效率。

然后,就开始对比hash表中的数据是否与目标数据相等while (weak_table->weak_entries[index].referent != referent),如果不相等,则index +1, 直到index == begin(绕了一圈)或超过了可能的hash冲突最大值。

参数(两层结构)
weak_table_t:参数一:weak_entry_t *weak_entries;

weak_entry_t的结构和weak_table_t很像,同样也是一个hash表,其存储的元素是weak_referrer_t,实质上是弱引用该对象的指针的指针(要操作指针的值,所以要用指针的指针操作),即 objc_object **new_referrer , 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。
weak_entries是数组,通过循环遍历来找到对应的entry。
上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组。

weak_entry_t 的参数
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    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的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。通过out_of_line()这样一个函数方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。

  • 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_table_t:参数二:num_entries

用来维护保证数组始终有一个合适的size。数组中元素的数量超过3/4的时候将数组的大小乘以2。

在这里插入图片描述
这是添加弱引用append_referrer方法中的代码,超过3/4时扩容。

流程

retain NSObject.mm line:1402-1417

//1、通过对象内存地址,在SideTables找到对应的SideTable
SideTable& table = SideTables()[this];

//2、通过对象内存地址,在refcnts中取出引用计数
//这里是table是SideTable。refcnts是RefcountMap
size_t& refcntStorage = table.refcnts[this];

//3、判断PINNED位,不为1则+4
if (! (refcntStorage & PINNED)) {
    refcntStorage += (1UL<<2);
}

加4因为引用计数的实际值从二进制的第三位开始存。

relesae NSObject.mm line:1524-1551

table.lock();
引用计数器 = table.refcnts.find(this);
//table.refcnts.end()表示使用一个iterator迭代器到达了end()状态
if (引用计数器 == table.refcnts.end()) {
    //标记对象为正在释放
    table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (引用计数器 < SIDE_TABLE_DEALLOCATING) {
    //这里很有意思,当出现小余(1UL<<1) 的情况的时候
    //就是前面引用计数位都是0,后面弱引用标记位WEAKLY_REFERENCED可能有弱引用1
    //或者没弱引用0

    //为了不去影响WEAKLY_REFERENCED的状态
    引用计数器 |= SIDE_TABLE_DEALLOCATING;
} else if ( SIDE_TABLE_RC_PINNED位为0) {
    引用计数器 -= SIDE_TABLE_RC_ONE;
}
table.unlock();
如果做完上述操作后如果需要释放对象,则调用dealloc

dealloc NSObject.mm line:1555-1571

dealloc操作也做了大量了逻辑判断和其它处理,咱们这里抛开那些逻辑只讨论下面部分sidetable_clearDeallocating()

SideTable& table = SideTables()[this];
table.lock();
引用计数器 = table.refcnts.find(this);
if (引用计数器 != table.refcnts.end()) {
    if (引用计数器中SIDE_TABLE_WEAKLY_REFERENCED标志位为1) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //从refcnts中删除引用计数器
    table.refcnts.erase(it);
}
table.unlock();

weak_clear_no_lock()是关键,它才是在对象被销毁的时候处理所有弱引用指针的方法。

weak_clear_no_lock objc-weak.mm line:461-504

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //1、拿到被销毁对象的指针
    objc_object *referent = (objc_object *)referent_id;

    //2、通过 指针 在weak_table中查找出对应的entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    //3、将所有的引用设置成nil
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        //3.1、如果弱引用超过4个则将referrers数组内的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        //3.2、不超过4个则将inline_referrers数组内的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    //循环设置所有的引用为nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[I];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    //4、从weak_table中移除entry
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值