本文源码来自于 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()[this];
- // 取出SideTable中存储的refcnts,类型为Map
- size_t& refcntStorage = table.refcnts[this];
- // 记录原始的引用计数器
- 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++ 语法,表示返回引用类型,看个例子:
&的用法 & 的用法还有些限定,比如不能返回栈中的引用,否则会栈变量消失后会出现 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[sizeof(StripedMap<SideTable>)];
复制代码 起首看解释,分析白两点:
- 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[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];
- }
- ...省略了对象方法...
- }
复制代码 上述代码的逻辑为:
- 根据是否为 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];
复制代码 上述代码就是通过 this ,即 object 对象的地址,取出 refcnts 这个哈希表中存储的引用计数器;
refcnts 可以明白成一个 Map,使用 address:refcount 的形式存储了许多个对象的引用计数器;
6. 总结
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 是什么,又是怎么用的呢,弱引用的存储逻辑是怎样的?
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 的初始化
image.png image.png image.png image.png image.png notifyBatchPartial 方法中,state = dyld_image_state_bound 时:
- (*sNotifyObjCMapped)(objcImageCount, paths, mhs);
复制代码 image.png image.png image.png
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |