SideTable
Side Table主要用于存储和管理对象的额外信息,特别是与弱引用相关的数据。Side Table的设计和使用是Objective-C运行时实现弱引用的基础,使得ARC(Automatic Reference Counting)能够正确地处理弱引用的生命周期。
新版本的 objc 中引入了 Tagged Pointer,且 isa 采用 union 的方式进行构造,其中 isa 的结构体中有一个 extra_rc
和 has_sidetable_rc
,这两者共同记录引用计数器。
在进行retain操作时,会调用rootRetain
方法
objc_object::rootRetain()
方法,只看 extra_rc 超出之后 sidetable 相关的代码,删减之后如下:
objective-c
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()
:
objective-c
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定义
objective-c
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
SideTablesde的实质类型是存储SideTable的StripedMap 。在StripedMap类中有StripeCount 定义存储sidetable的最大数量,所以每个SideTablesdes可以对应多个对象,而每个对象对应一个sidetable。
SideTableBuf
objective-c
// 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 本质上是一个长度为sizeof(StripedMap) 的 char 类型的数组;同时也可以这么理解:
SideTableBuf 本质上就是一个大小为和 StripedMap<SideTable>
对象一致的内存块;
这也是为什么 SideTableBuf 可以用来表示 StripedMap<SideTable>
对象。本质上而言,SideTableBuf
就是指一个 StripedMap<SideTable>
对象;
StripedMap < SideTable >
StripedMap 是一个模板类,在这个类中有一个 array 成员,用来存储 PaddedT 对象,并且其中对于 [] 符的重载定义中,会返回这个 PaddedT 的 value 成员,这个 value 就是我们传入的 T 泛型成员,也就是 SideTable 对象。在 array 的下标中,这里使用了 indexForPointer 方法通过位运算计算下标,实现了静态的 Hash Table。而在 weak_table 中,其成员 weak_entry 会将传入对象的地址加以封装起来,并且其中也有访问全局弱引用表的入口。
objective-c
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-1),所以不会出现数组越界; -
后面的
operator
表示重写了运算符 [] 的逻辑,调用了indexForPointer()
方法,这样使用起来更像一个数组;
SideTables 可以理解成一个类型为 StripedMap< SideTable> 静态全局对象,内部以数组的形式存储了 StripeCount 个 SideTable;
SideTable
objective-c
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
//构造函数
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
//析构函数
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
...省略对象方法...
}
slock是一个自旋锁,就是为了保证多线程访问安全性
refcnts本质是一个存储对象引用计数的hash表,key为对象,value为引用计数(优化过得isa中,引用计数主要存储在isa中)
weak_table是存储对象弱引用的一个结构体,该结构体内成员如下:
objective-c
/**
全局的弱引用表, 保存object作为key, weak_entry_t作为value
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
// 保存了所有指向特地对象的 weak指针集合
weak_entry_t *weak_entries;
// weak_table_t中有多少个weak_entry_t
size_t num_entries;
// weak_entry_t数组的count
uintptr_t mask;
// hash key 最大偏移值,
// 采用了开放定制法解决hash冲突,超过max_hash_displacement说明weak_table_t中不存在要找的weak_entry_t
uintptr_t max_hash_displacement;
};
下面是refcnts的定义:
objective-c
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
DenseMap
是一个 hash Map,基类 DenseMapBase
中的部分代码,如下,DenseMapBase
中重写了操作符 []:
objective-c
ValueT &operator[](const KeyT &Key) {
return FindAndConstruct(Key).second;
}
通过传入的 Key 寻找对应的 Value。而 Key 是 DisguisedPtr<objc_object>
类型,Value 是 size_t
类型。即使用 obj.address :refCount 的形式来记录引用计数器;
回到最初的 sidetable_addExtraRC_nolock
方法中:
objective-c
size_t& refcntStorage = table.refcnts[this];
通过 this
,即 object 对象的地址,取出 refcnts
这个哈希表中存储的引用计数器;
refcnts 可以理解成一个 Map,使用 address:refcount 的形式存储了很多个对象的引用计数器;
总结
-
iphone 中 SideTables() 本质是返回一个 SideTableBuf 对象,该对象存储 8 个 SideTable;
-
因为涉及到多线程和效率的问题,必定不可能只使用一个 SideTable 来存储对象相关的引用计数器和弱引用;
-
Apple 通过对 object 的地址进行运算之后,对 SideTable 的个数进行取模运算,以此来决定将对象分配到哪个 SideTable 进行信息存储,因为有取模运算,不会出现数组溢出的情况;
-
当对象需要使用到 sideTable 时,会被分配到 8/64 个全局 sideTables 中的某一个表中存储相关的引用计数器或者弱引用信息;