半亩花草 发表于 2025-1-18 11:40:12

iOS:SideTable

本文源码来自于 objc4-756.2 版本;
本文研究 sideTable 在 objc4 源码中的使用及其作用,从而剖析 iOS 中引用计数器和弱引用的实现原理;
1. retain 利用

我们都知道,新版本的 objc 中引入了 Tagged Pointer,且 isa 采用 union 的方式进行构造,此中 isa 的布局体中有一个 extra_rc 和 has_sidetable_rc,这两者共同记录引用计数器。
直接看看 objc_object::rootRetain() 方法,只看 extra_rc 超出之后 sidetable 相关的代码,删减之后如下:
uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);// extra_rc++

if (carry) {
    // Leave half of the retain counts inline and prepare to copy the other half to the side table.
    transcribeToSideTable = true;
    newisa.extra_rc = RC_HALF;
    newisa.has_sidetable_rc = true;
}
if (slowpath(transcribeToSideTable)) {
      // Copy the other half of the retain counts to the side table.
      sidetable_addExtraRC_nolock(RC_HALF);
} 那么关键方法就是 sidetable_addExtraRC_nolock():
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    // 取出this对象所在的SideTable
    SideTable& table = SideTables();
    // 取出SideTable中存储的refcnts,类型为Map
    size_t& refcntStorage = table.refcnts;
    // 记录原始的引用计数器
    size_t oldRefcnt = refcntStorage;

    // 容错处理
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt =
      addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
      // SideTable溢出处理
      refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
      return true;
    } else {
      // SideTable未溢出
      refcntStorage = newRefcnt;
      return false;
    }
} 这个函数的逻辑如下:

[*]根据 this,也就是对象的地址从 SideTables 中取出一个 SideTable;
[*]获取 SideTable 的 refcnts,这个成员变量是一个 Map;
[*]存储旧的引用计数器;
[*]进行 add 盘算,并记录是否有溢出;
[*]根据是否溢出盘算并记录结果,最后返回;
那么,这里有几个点需要解开:

[*]什么是 SideTables;
[*]什么是 SideTable;
[*]什么是 refcnts;
[*]add 的盘算逻辑为什么需要位移?
[*]SideTable 中的溢出时如何处置处罚的?
接下来,一一办理~~~
2. SideTables

直接来看 SideTables 的代码:
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
} 起首,这是个静态函数,返回 StripedMap<SideTable> 类型,但是 & 是什么意思呢?这个是 C++ 语法,表示返回引用类型,看个例子:
          https://i-blog.csdnimg.cn/blog_migrate/9d3a45aca174e92acc8293a778c9753e.png          &的用法    & 的用法还有些限定,比如不能返回栈中的引用,否则会栈变量消失后会出现 error,还有一些其他的限定,有兴趣可以穷究,这里只需要知道 & 表示返回引用类型,也就是可以通过 & func() 来获取函数返回值的指针,其他的不再赘述;
接着,比较懵逼的是 *reinterpret_cast ,其实这个是 C++ 的逼迫类型转换语法,不用穷究,有兴趣的可以自行百度。
以是,总结下这段代码:

[*]SideTables() 使用 static 修饰,是一个静态函数;
[*]& 表示返回引用类型;
[*]reinterpret_cast 是一个逼迫类型转换符号;
[*]函数最终的结果就是返回 SideTableBuf;
那么 SideTableBuf 又是什么?
3. SideTableBuf

直接看代码:
// We cannot use a C++ static initializer to initialize SideTables because
// libc calls us before our C++ initializers run. We also don't want a global
// pointer to this struct because of the extra indirection.
// Do it the hard way.

alignas(StripedMap<SideTable>) static uint8_t
    SideTableBuf; 起首看解释,分析白两点:

[*]SideTables 在 C++ 的 initializers 函数之前被调用,以是不能使用 C++ 初始化函数来初始化 SideTables,而 SideTables 本质就是 SideTableBuf;
[*]不能使用全局指针来指向这个布局体,因为涉及到重定向问题;
   其实照旧比较懵逼为什么 SideTableBuf 要这么设计,原理有待讲求~~~估计和初始化有关;
继续看 SideTableBuf,要点包括:

[*]alignas 表示对齐;
[*]StripedMap<SideTable> 的 size 为 4096(存疑,待验证);
[*]uint8_t 现实上是 unsigned char 类型,即占 1 个字节;
由此可以得出:


[*]SideTableBuf 本质上是一个长度为 sizeof(StripedMap<SideTable>) 的 char 类型的数组;
同时也可以这么明白:


[*]SideTableBuf 本质上就是一个巨细为和 StripedMap<SideTable> 对象同等的内存块;
这也是为什么 SideTableBuf 可以用来表示 StripedMap<SideTable> 对象。本质上而言,SideTableBuf 就是指一个 StripedMap<SideTable>对象;
那么接下来就是搞清楚 StripedMap<SideTable> 是个什么东西了......
4. StripedMap<SideTable>

先上代码,删减一些方法之后的代码为:
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;

    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.value;
    }
    const T& operator[] (const void *p) const {
      return const_cast<StripedMap<T>>(this);
    }
    ...省略了对象方法...
} 上述代码的逻辑为:

[*]根据是否为 iphone 定义了一个 StripeCount,iphone 下为 8;
[*]源码中 CacheLineSize 为 64,使用 T 定义了一个布局体,而 T 就是 SideTable 类型;
[*]天生了一个长度为 8 类型为 SideTable 的数组;
[*]indexForPointer() 逻辑为根据传入的指针,经过一定的算法,盘算出一个存储该指针的位置,因为使用了取模运算,以是值为 0 - StripeCount;
[*]背面的 operator 表示重写了运算符 [] 的逻辑,调用了 indexForPointer() 方法,这样使用起来更像一个数组;
至此,SideTables 的寄义已经很清楚了:


[*]SideTables 可以明白成一个类型为 StripedMap<SideTable> 静态全局对象,内部以数组的形式存储了 StripeCount 个 SideTable;
那么第一个问题已包办理,按照 sidetable_addExtraRC_nolock() 方法中的逻辑,先从 SideTables 数组中取出一个 SideTable,然后进行相关利用,以是现在就来看看 SideTable 是个啥~~~
5. SideTable

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.");
    }
    ...省略对象方法...
} 可以看到,SideTable 有三个成员变量:

[*]spinlock_t:自旋锁,负责加锁相关逻辑;
[*]refcnts:存储引用计数器的 Map;
[*]weak_table:存储弱引用的表;
自旋锁暂不讨论,来看看 refcnts 的定义:
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap; DenseMap 过于复杂,我们看看基类 DenseMapBase 中的部门代码,如下,DenseMapBase中重写了利用符 []:
ValueT &operator[](const KeyT &Key) {
    return FindAndConstruct(Key).second;
} 大意是通过传入的 Key 寻找对应的 Value。而 Key 是 DisguisedPtr<objc_object> 类型,Value 是 size_t 类型。
回到最初的 sidetable_addExtraRC_nolock 方法中:
size_t& refcntStorage = table.refcnts; 上述代码就是通过 this ,即 object 对象的地址,取出 refcnts 这个哈希表中存储的引用计数器;
refcnts 可以明白成一个 Map,使用 address:refcount 的形式存储了许多个对象的引用计数器;
6. 总结

          https://i-blog.csdnimg.cn/blog_migrate/bf766aa74327a50b2ad76ca7a9dd0a5c.png          SideTables 和 SideTable   
[*]iphone 中 SideTables() 本质是返回一个 SideTableBuf 对象,该对象存储 8 个 SideTable;
[*]因为涉及到多线程和服从的问题,肯定不可能只使用一个 SideTable 来存储对象相关的引用计数器和弱引用;
[*]Apple 通过对 object 的地址进行运算之后,对 SideTable 的个数进行取模运算,以此来决定将对象分配到哪个 SideTable 进行信息存储,因为有取模运算,不会出现数组溢出的环境;
总结:


[*]objc 中当对象需要使用到 sideTable 时,会被分配到 8/64 个全局 sideTables 中的某一个表中存储相关的引用计数器或者弱引用信息;
7. weak_table

继续看弱引用如何实现的,看看 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;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
}; 解释也分析白 weak_table_t 是一个全局引用表,object 的地址作为 key,weak_entry_t 作为 Value。只不过这个全局引用表有 8 或者 64 个;
那么这个 weak_entry_t 是什么,又是怎么用的呢,弱引用的存储逻辑是怎样的?
          https://i-blog.csdnimg.cn/blog_migrate/136b245dbcc8c6e1a66da11b540a2301.png          image.png    疑问


[*]为什么 weak 能够自动置位 nil?
[*]析构函数?
SideTables 的初始化时机和流程

isa 中的 shiftcls 溢出时则调用 sidetable_addExtraRC_nolock 方法将一半的引用计数器存入 sidetable 中
从上面的代码来看,逻辑相对清楚:

[*]SideTables 应该是一个全局性的东西;
[*]传入 this 取出 当前对象相关的 SideTable;
[*]SideTable 有一个 refcnts 的 map,仍然根据 this 取出这个旧的引用计数器;
[*]通过 addc 盘算,需要位移估计是因为 refcnts 在 sideTable 中的位置有关吧;
[*]盘算完毕,如果没有溢出,则直接新值替换旧值;
[*]如果发生溢出,则??
接下来 SideTable:
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    ...省略对象方法...
} SideTables 应该是一个全局变量,其定义如下:
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
} 如上,是一个静态方法,获取 StripedMap<SideTable> 类型的变量的
SideTables 的初始化

          https://i-blog.csdnimg.cn/blog_migrate/4bca5032e99cbf0afe954aed07c8eaac.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/c00c405ae9cdcf67fd2fa49f33d801cc.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/e712fcfeb93c4d3fc36c5954c96f6e50.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/af02dda02a30d5c78a98d49a351458b3.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/a7a714ecd75ced41af320c316cd199f0.png          image.png    notifyBatchPartial 方法中,state = dyld_image_state_bound 时:
(*sNotifyObjCMapped)(objcImageCount, paths, mhs);          https://i-blog.csdnimg.cn/blog_migrate/e01e86c83082a021b559fb49a30173c5.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/e877b52263fcf4231335e12a3f029459.png          image.png             https://i-blog.csdnimg.cn/blog_migrate/d40115ee25198ccf03f0153c6a740b4c.png          image.png
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: iOS:SideTable