主要参考文章
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;
};
列举几个比较重要的成员变量:
-
ZeroValuesArePurgeable默认值是false,但 RefcountMap 指定其初始化为true. 这个成员标记是否可以使用值为 0 (引用计数为 1) 的桶. 因为空桶存的初始值就是 0, 所以值为 0 的桶和空桶没什么区别. 如果允许使用值为 0 的桶, 查找桶时如果没有找到对象对应的桶, 也没有找到墓碑桶, 就会优先使用值为 0 的桶.
-
Buckets指针管理一段连续内存空间, 也就是数组, 数组成员是BucketT 类型的对象, 我们这里将 BucketT 对象称为桶(实际上这个数组才应该叫桶, 苹果把数组中的元素称为桶应该是为了形象一些, 而不是哈希桶中的桶的意思). 桶数组在申请空间后, 会进行初始化, 在所有位置上都放上空桶(桶的 key为 EmptyKey 时是空桶), 之后对引用计数的操作, 都要依赖于桶.
引用计数结构bucketT实际上是std::pair,类似于isa,使用其中几个bit来保存引用计数,留出几个bit用来做其他标记位-
(1UL<<0) WEAKLY_REFERENCED
表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。 -
(1UL<<1) DEALLOCATING
表示对象是否正在被释放。1正在释放,0没有。 -
REAL COUNT
REAL COUNT的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四,因为真正的计数是从2^2位开始的。 -
(1UL<<(WORD_BITS-1)) SIDE_TABLE_RC_PINNED
其中WORD_BITS在32位和64位系统的时候分别等于32和64。随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。
-
-
umEntries记录数组中已使用的非空的桶的个数.
-
NumTombstones,Tombstone直译为墓碑, 当一个对象的引用计数为0, 要从桶中取出时, 其所处的位置会被标记为 Tombstone.NumTombstones就是数组中的墓碑的个数. 后面会介绍到墓碑的作用.
-
NumBuckets桶的数量, 因为数组中始终都充满桶, 所以可以理解为数组大小
RefcountMap工作逻辑
- 过计算对象地址的哈希值, 来从SideTables中获取对应的SideTable. 哈希值重复的对象的引用计数存储在同一个 SideTable里.
- 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